セキュリティ
アプリを開発する際、セキュリティはしばしば見落とされがちです。完全に破られないソフトウェアを構築することは不可能であるというのは事実です。完全に破られない鍵はまだ発明されていません(結局のところ、銀行の金庫も侵入されることがあります)。しかし、悪意のある攻撃の犠牲になったり、セキュリティの脆弱性が露呈したりする確率は、アプリケーションをそのような事態から保護するためにどれだけの努力を惜しまないかに反比例します。普通の南京錠はピッキング可能ですが、それでもキャビネットのフックよりも突破するのははるかに困難です!
このガイドでは、機密情報の保存、認証、ネットワークセキュリティ、およびアプリを保護するのに役立つツールに関するベストプラクティスについて学びます。これは飛行前のチェックリストではなく、アプリとユーザーをさらに保護するのに役立つオプションのカタログです。
機密情報の保存
機密性の高いAPIキーをアプリのコードに保存しないでください。コードに含まれるものはすべて、アプリのバンドルを検査する人によってプレーンテキストでアクセスされる可能性があります。react-native-dotenvやreact-native-configのようなツールは、APIエンドポイントなどの環境固有の変数を追加するのに最適ですが、秘密やAPIキーを含むことが多いサーバーサイドの環境変数と混同すべきではありません。
アプリから特定のリソースにアクセスするためにAPIキーやシークレットが必要な場合、最も安全な方法は、アプリとリソースの間にオーケストレーションレイヤーを構築することです。これは、必要なAPIキーやシークレットとともにリクエストを転送できるサーバーレス関数(例: AWS LambdaやGoogle Cloud Functionsを使用)である可能性があります。サーバーサイドのコード内のシークレットは、アプリのコード内のシークレットと同じ方法でAPIコンシューマーがアクセスすることはできません。
永続化されたユーザーデータについては、その機密性に基づいて適切な種類のストレージを選択してください。 アプリが使用されるにつれて、オフラインでのアプリの使用をサポートするため、ネットワークリクエストを減らすため、またはセッション間でユーザーのアクセストークンを保存してアプリを使用するたびに再認証する必要がないようにするためなど、デバイスにデータを保存する必要があることがよくあります。
永続化されたデータと永続化されていないデータ — 永続化されたデータはデバイスのディスクに書き込まれるため、アプリの起動後もネットワークリクエストを再度実行して取得したり、ユーザーに再入力したりすることなく、アプリでデータを読み取ることができます。しかし、これはそのデータが攻撃者によってアクセスされやすくなる可能性も高めます。永続化されていないデータはディスクに書き込まれないため、アクセスできるデータはありません!
非同期ストレージ
Async Storageは、非同期で暗号化されていないキーと値のストアを提供する、コミュニティによって維持されているReact Nativeのモジュールです。Async Storageはアプリ間で共有されません。各アプリは独自のサンドボックス環境を持ち、他のアプリのデータにアクセスできません。
| Async Storageは、次のような場合に使用してください。 | Async Storageは、次のような場合には使用しないでください。 |
|---|---|
| アプリの実行間で機密性のないデータを永続化する | トークンストレージ |
| Reduxステートの永続化 | 秘密情報 |
| GraphQLステートの永続化 | |
| グローバルなアプリ全体の変数を保存する |
開発者メモ
- Web
Async Storageは、ウェブのLocal Storageに相当するReact Nativeの機能です。
セキュアストレージ
React Nativeには、機密データを保存する方法がバンドルされていません。しかし、AndroidおよびiOSプラットフォームには既存のソリューションがあります。
iOS - キーチェーンサービス
キーチェーンサービスを使用すると、ユーザーの機密情報を安全に保存できます。これは、証明書、トークン、パスワード、およびAsync Storageに属さないその他の機密情報を保存するのに理想的な場所です。
Android - セキュア共有設定
Shared Preferencesは、永続的なキーと値のデータストアに相当するAndroidの機能です。Shared Preferencesのデータは、デフォルトでは暗号化されませんが、Encrypted Shared Preferencesは、AndroidのShared Preferencesクラスをラップし、キーと値を自動的に暗号化します。
Android - キーストア
Android Keystoreシステムを使用すると、暗号化キーをコンテナに保存し、デバイスから抽出しにくくすることができます。
iOSキーチェーンサービスまたはAndroidセキュア共有設定を使用するには、自分でブリッジを作成するか、それらをラップして統一されたAPIを自己責任で提供するライブラリを使用できます。検討すべきライブラリの一部:
意図せず機密情報を保存したり公開したりしないように注意してください。 これは、例えば、機密性の高いフォームデータをReduxの状態に保存し、その状態ツリー全体をAsync Storageに永続化したり、ユーザーのトークンや個人情報をSentryやCrashlyticsなどのアプリケーション監視サービスに送信したりすることで、偶発的に発生する可能性があります。
認証とディープリンク
モバイルアプリには、ウェブには存在しない独自の脆弱性があります。それはディープリンクです。ディープリンクは、外部ソースからネイティブアプリケーションにデータを直接送信する方法です。ディープリンクはapp://のようになり、appはアプリスキームであり、//に続くものはすべてリクエストを処理するために内部的に使用できます。
たとえば、eコマースアプリを構築している場合、app://products/1を使用してアプリにディープリンクし、IDが1の製品の詳細ページを開くことができます。これらはウェブのURLのようなものと考えることができますが、1つの重要な違いがあります
ディープリンクは安全ではないため、機密情報を送信しないでください。
ディープリンクが安全でない理由は、URLスキームを登録する集中型メソッドがないためです。アプリケーション開発者は、iOSの場合はXcodeで設定するか、Androidの場合はAndroidでインテントを追加することで、ほとんど任意のURLスキームを使用できます。
悪意のあるアプリケーションが、同じスキームに登録してディープリンクをハイジャックし、リンクに含まれるデータへのアクセス権を取得することを止めるものはありません。app://products/1のようなものを送信することは無害ですが、トークンを送信することはセキュリティ上の懸念です。
オペレーティングシステムがリンクを開くときに2つ以上のアプリケーションから選択する場合、Androidはユーザーに曖昧さ解消ダイアログを表示し、リンクを開くために使用するアプリケーションを選択するよう求めます。しかし、iOSでは、オペレーティングシステムがユーザーの代わりに選択するため、ユーザーは何も知らないままになります。Appleはこの問題に対処するために、後のiOSバージョン(iOS 11)で先着順の原則を導入しましたが、この脆弱性はまだ異なる方法で悪用される可能性があり、詳細についてはこちらで読むことができます。ユニバーサルリンクを使用すると、iOSでアプリ内のコンテンツに安全にリンクできます。
OAuth2とリダイレクト
OAuth2認証プロトコルは、今日では非常に人気があり、最も完全で安全なプロトコルとして誇られています。OpenID Connectプロトコルもこれに基づいています。OAuth2では、ユーザーはサードパーティ経由で認証を求められます。認証が成功すると、このサードパーティは、JWT(JSON Web Token)と交換できる検証コードとともに、要求元のアプリケーションにリダイレクトします。JWTは、ウェブ上で当事者間で情報を安全に送信するためのオープン標準です。
ウェブでは、このリダイレクトステップは安全です。なぜなら、ウェブ上のURLは一意であることが保証されているからです。しかし、アプリではそうではありません。前述のとおり、URLスキームを登録する集中型メソッドがないからです!このセキュリティ上の懸念に対処するために、PKCEという形で追加のチェックを追加する必要があります。
PKCE(「Pixy」と発音)はProof of Key Code Exchangeの略で、OAuth 2仕様の拡張です。これは、認証リクエストとトークン交換リクエストが同じクライアントから送信されたものであることを検証する追加のセキュリティ層を追加するものです。PKCEはSHA 256暗号学的ハッシュアルゴリズムを使用します。SHA 256は、任意のサイズのテキストまたはファイルに対して一意の「署名」を作成しますが、
- 入力ファイルに関係なく、常に同じ長さである
- 同じ入力に対して常に同じ結果を生成することが保証されている
- 一方向(つまり、元の入力を明らかにするために逆エンジニアリングできない)
これで2つの値が得られました
- code_verifier - クライアントによって生成される大きなランダム文字列
- code_challenge - code_verifierのSHA 256
最初の/authorizeリクエスト中に、クライアントはメモリに保持しているcode_verifierのcode_challengeも送信します。承認リクエストが正しく返された後、クライアントはcode_challengeの生成に使用したcode_verifierも送信します。IDPはcode_challengeを計算し、最初の/authorizeリクエストで設定されたものと一致するかどうかを確認し、値が一致する場合にのみアクセストークンを送信します。
これにより、最初の認証フローをトリガーしたアプリケーションのみが、検証コードをJWTと正常に交換できることが保証されます。そのため、悪意のあるアプリケーションが検証コードにアクセスできたとしても、それだけでは役に立ちません。実際の動作を確認するには、この例をチェックしてください。
ネイティブOAuthを検討するライブラリとしてreact-native-app-authがあります。React-native-app-authは、OAuth2プロバイダーと通信するためのSDKです。ネイティブのAppAuth-iOSおよびreact-native-app-authは、IDプロバイダーがサポートしている場合にのみPKCEをサポートできます。
ネットワークセキュリティ
APIは常にSSL暗号化を使用する必要があります。SSL暗号化は、要求されたデータがサーバーから離れてクライアントに到達するまでの間にプレーンテキストで読み取られるのを防ぎます。エンドポイントがhttp://ではなくhttps://で始まるため、安全であることを知るでしょう。
SSLピンニング
HTTPSエンドポイントを使用しても、データが傍受される脆弱性が残る可能性があります。HTTPSでは、クライアントにプリインストールされている信頼された認証局によって署名された有効な証明書をサーバーが提供できる場合にのみ、クライアントはサーバーを信頼します。攻撃者は、悪意のあるルートCA証明書をユーザーのデバイスにインストールすることでこれを悪用する可能性があります。そうすると、クライアントは攻撃者によって署名されたすべての証明書を信頼してしまいます。したがって、証明書のみに頼ると、中間者攻撃に対して脆弱なままになる可能性があります。
SSLピンニングは、この攻撃を回避するためにクライアント側で使用できる技術です。開発中に信頼された証明書のリストをクライアントに埋め込む(またはピン留めする)ことで機能し、信頼された証明書のいずれかで署名されたリクエストのみが受け入れられ、自己署名証明書は受け入れられません。
SSLピンニングを使用する際は、証明書の有効期限に注意する必要があります。証明書は1〜2年ごとに有効期限が切れ、有効期限が切れた場合は、サーバーだけでなくアプリでも更新する必要があります。サーバー上の証明書が更新されるとすぐに、古い証明書が組み込まれているアプリは動作しなくなります。
まとめ
セキュリティを完全に保証する方法はありませんが、意識的な努力と注意を払うことで、アプリケーションのセキュリティ侵害の可能性を大幅に減らすことができます。アプリケーションに保存されているデータの機密性、ユーザー数、およびハッカーがアカウントにアクセスした場合に引き起こす可能性のある損害に比例してセキュリティに投資してください。そして覚えておいてください。そもそも要求されなかった情報にアクセスすることははるかに困難です。