モダンなアプリでアクセストークンを最も安全に保存する方法は、永続化しないことです。セッション中は短命なアクセストークンをメモリに保持し、プラットフォームがバックアップする暗号化ストレージに保存されたリフレッシュトークンから、起動のたびに新しいトークンを発行します。iOSではリフレッシュトークンのみを Keychain Androidでは Android Keystore (デバイスに応じてソフトウェア、TEE、またはStrongBox)と、 Jetpack DataStore + Tink. EncryptedSharedPreferences は非推奨です における androidx.security.crypto; 今すぐ移行を計画してください。For Capacitor アプリでは、トランスポートセキュリティ(HTTPS、 SSL pinningツールやプラグイン、)があり、 ネイティブ実装のストレージプラグイン と @capgo/capacitor-native-biometric JavaScriptで暗号処理を二重実装する代わりに。
ストレージパターンの選定基準
本記事のすべての推奨事項は、4つのフィルターを通過しています。第一に、プラットフォームが保護するストレージ: シークレットはKeychainまたはKeystoreに保存するか、そこに保存されたキーで暗号化する必要があります。デバイスとキー設定が実際にそれを提供する場合は、ハードウェアによる分離を優先してください(StrongBoxやSecure Enclaveで保護されたキーなど)。第二に、公式にサポートされた移行パス。GoogleまたはAppleがそのアプローチを非推奨にしている場合、新しいcodeには推奨しません。第三に、Capacitorとの互換性: このパターンは、生のキーをJavaScriptに公開せずにプラグイン境界から動作する必要があります。第四に、失効可能性: 漏洩した認証情報はサーバー側で無効化できなければなりません。
最後のフィルターこそが、本記事全体でリフレッシュトークンのローテーションが登場する理由です。Chainalysisによると、2024年の秘密鍵の漏洩により、日本のDMM Bitcoinから3億500万ドル以上が盗まれたと報告されています(Chainalysis 2024年犯罪レポート)。モバイルの認証トークンも同じように失敗します。プレーンテキストで保存された有効期限の長いベアラートークンは、攻撃者を招き入れているようなものです。ローテーションしない認証情報を無期限に永続化するアーキテクチャは、一切推奨しません。
あらゆるプラットフォームに適用できる原則
以下の表はスナップショットにまとめたものですが、根底にあるロジックは access/refresh トークンの分離です。access トークン アクセストークン は短命で、数日ではなく数分間しか有効ではありません。API が提示するものです。影響範囲が小さく、すぐに失効するため、メモリ内にのみ保持し、アプリの起動ごとに再構築する必要があります。 refresh トークン は、新しい access トークンを発行できる永続的なシークレットなので、これだけがハードウェアで保護された永続暗号化ストレージに保存する価値があります。有効期限が異なれば保存場所も分けるべきで、両方を同じ方法で保存することが多くの漏洩の始まりです。
実行時の完全なシーケンスは以下の通りです。アプリはアクセストークンなしで起動し、永続化されたリフレッシュトークンを使って認証サーバーを呼び出し、メモリ内に新しいアクセストークンを受け取ります。サーバーは使用のたびにリフレッシュトークンをローテーションするため、古いリフレッシュトークンは破棄されます。サーバーがすでに消費済みのリフレッシュトークンを再度提示された場合、それを盗用とみなし、トークンファミリー全体を失効させます。ログアウト時、クライアントはローカルストレージを消去し、サーバーは認可を失効させます。サーバーでの失効なしにローカルデータを削除することは、セキュリティではなく願望に過ぎません。
| 実践 | なぜ重要か |
|---|---|
| 短命な access トークン | トークン漏洩時の被害を抑える |
| refresh トークンのローテーション | 正規の使用後に盗まれた refresh トークンを無効化する |
| HTTPSのみ | 安易なネットワーク傍受を防止 |
| 証明書ピンニング(必要な場合) | 不適切に発行された証明書やカスタムルートによるリスクを軽減。OWASPの証明書ピンニングガイドを参照してください |
| ログ、URL、アナリティクスにトークンを残さない | オブザーバビリティツール内のトークンは漏洩インシデントになります |
| ログアウト時に失効 | クライアント側のデータ削除に頼るより、サーバー側での無効化が確実です |
@capgo/capacitor-ssl-pinning より厳格なトランスポート制御が必要な場合、iOS と Android で Capacitor HTTP の TLS をピン留めできます。トランスポート強化のより広い視点については、 SSL ピニングの実装ツールとプラグイン.
Android: KeystoreとDataStore(EncryptedSharedPreferencesは使用しない)
Googleは次を非推奨にしました androidx.security.crypto ヘルパー(以下を含む) EncryptedSharedPreferences と MasterKey。パッケージの概要では、開発者に対して KeyGenerator を AndroidKeyStore プロバイダーと組み合わせて使用することを 新しい鍵マテリアルに推奨しています。
推奨パターン
Tink ベースのパイプラインをエンドツーエンドで統一してください:
- Tink キーセットを
AndroidKeysetManagerで構築し、マスターキーをandroid-keystore://URI 経由で Android Keystore に保存します(datastore-tinkリリースノート)。呼び出しはisUsingKeystore()マネージャーを参照し、返却値がfalse. Tink はそれ以外の場合、キーセットを平文で保存します。 - を取得します。
Aeadそのキーセットからプリミティブを取得します。 - 追加
androidx.datastore:datastore-tink(DataStore 1.3.0 でアーティファクトが提供されます(1.2.x では提供されません)。Serializerを使用してAeadSerializerをラップし、DataStoreに渡します。複数のストアが1つのAeadキーを共有する場合、各ストアに一意のassociatedDataを割り当てます。例えば、ファイル名から導出された安定した値などです。
Google はこれを、非推奨の代替手段としてサポートされているパスとして文書化しています。 androidx.security.crypto helpers。スタンドアロンのJCAを混在させないでください SecretKey を使用して AeadSerializer; シリアライザはTink Aead インスタンスを期待しています。
知っておくべきKeystoreのオプション
setIsStrongBoxBacked(true)いつFEATURE_STRONGBOX_KEYSTOREより強力なキー分離のために利用できます。- ユーザー認証 バインディング(
setUserAuthenticationRequired)生体認証またはデバイス認証情報の後でのみトークンにアクセスできるようにする場合に使用します。 - Auto Backupを避ける キーセットや暗号文を保持するファイルについては、復元時の挙動を理解していない限り注意してください。Google の EncryptedSharedPreferences のドキュメントも、バックアップでキーなしに暗号文が復元されることについて警告していました。
Capacitor 内のネイティブ Kotlin または Java モジュールは、プラグイン内でこのパターンを実装すべきです。JavaScript には生のキーではなく、opaque なハンドルやセッション状態だけを渡すようにしてください。
iOS: Keychain と Secure Enclave
この Keychain は、iOS でトークンを保存する正しい場所であり続けます。アイテムは暗号化され、システムで保護されます。範囲を絞ったアクセシビリティ定数を使い、 アクセス制御 Face ID、Touch ID、パスコードによる保護が必要な場合に使います。
アクセシビリティの選択は、コピーするデフォルトではなく実際の判断です。次を選んでください: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly トークンをバックグラウンド作業(プッシュトリガーのリフレッシュやバックグラウンドフェッチなど)で使用可能にする必要があるが、バックアップ経由で新しいデバイスに移動させたくない場合。Pick kSecAttrAccessibleWhenUnlockedThisDeviceOnly フォアグラウンドでのみトークンを使い、より狭い利用可能時間が制約に見合う場合。どちらの場合も、次を有効にしてください: kSecUseDataProtectionKeychain 最新のターゲットで有効にすると、レガシーなファイルベースのものではなく、データ保護 Keychain を利用できます。
CryptoKit は、ペイロードを自分で暗号化する場合には役立ちますが、標準的な OAuth 形式のベアラートークンなら、適切なアクセシビリティ設定でトークン文字列を Keychain に直接保存するだけで通常は十分です。Secure Enclave ベースのキー(P-256 キーを SecureEnclave CryptoKit) は、鍵自体が安全なハードウェアから絶対に離れてはならない場合(署名操作やデバイス証明など)にのみ有効です。トークン文字列を保持することはできません。保護するのはご自身のエンベロープ鍵です。エンベロープのローテーションとそのライフサイクル管理を行わない場合、プレーンな Keychain ストレージの方がシンプルで、安全性も劣りません。アプリ層での追加暗号化は、鍵のローテーションとエンベロープの慎重な管理を併用した場合にのみ有効です。
Capacitor の @capacitor/preferences 書き込み先は UserDefaultsですが、これは 決して 機密ストレージではありません。トークンをそこに保存しないでください。
Capacitor: Cap-go org でのセキュアストレージの選択肢
Cap-go は 決して 「secure storage」という名前のパッケージを提供していますが、一般的な Capacitor のニーズには2つのプラグインで対応できます:
| プラグイン | 最適な用途 | ネイティブ実装 |
|---|---|---|
@capgo/capacitor-native-biometric setData / getSecureData |
小さな機密情報(セッショントークン、API キー) | iOS Keychain、Android KeystoreによるSharedPreferencesの暗号化 |
| @capgo/capacitor-data-storage-sqlite 暗号化オプション付き | 大容量または構造化されたローカルデータ | SQLite(SQLCipherはオプション) |
この キーバリューストレージガイド Preferences、SQLiteストア、および NativeBiometric.setData 機密情報用。
要件: @capgo/capacitor-native-biometric 8.6.0 以降 (プラグインのメジャーバージョンを Capacitor のメジャーバージョンに合わせてください)。生体認証による読み取りの例:
import {
AccessControl,
NativeBiometric,
} from '@capgo/capacitor-native-biometric';
const TOKEN_KEY = 'auth.refresh_token';
export async function saveRefreshToken(token: string) {
await NativeBiometric.setData({
key: TOKEN_KEY,
value: token,
accessControl: AccessControl.BIOMETRY_CURRENT_SET,
});
}
export async function loadRefreshToken(): Promise<string | null> {
try {
const { value } = await NativeBiometric.getSecureData({
key: TOKEN_KEY,
reason: 'Unlock your session',
title: 'Authenticate',
});
return value ?? null;
} catch {
return null;
}
}
getData 保存された値のみを返します なしで accessControl。生体認証による保護には、常に BIOMETRY_CURRENT_SET または BIOMETRY_ANY を使用して getSecureData.
Webビルドでは、本番用トークンを以下に永続化しないでください: localStorage。代わりにバックエンドのWebセッションモデルを使用してください。
サーバーとクライアントでのトークンライフサイクル
はい、モバイルアプリはリフレッシュトークンを保存すべきです。代替案は、起動のたびにユーザーに再認証を強制することですが、これは本物のセキュリティよりもむしろ「見せかけのセキュリティ」です。そのニュアンスは 「どこで」 と 次の通りです:リフレッシュトークンのみを、Keychain または Keystore によるバックアップ付きストレージにのみ保存し、かつその裏にローテーションとサーバー側の失効処理を備えることです。
モバイルアプリでリフレッシュトークンを安全に管理・ローテーションする方法
ローテーションこそが、永続化を安全にするものです。リフレッシュトークンは 使い捨て の認証情報として発行します:各利用時に新しいリフレッシュトークンを返し、古いものを無効にします。サーバーは短い 再利用検出ウィンドウを保持します。消費済みとされるトークンが再び提示された場合、そのファミリーが侵害されたと判断し、チェーン全体を失効させて、それを共有していたすべてのデバイスで新規ログインを強制します。競合するリクエストやオフラインからの再接続に対応するため、正当なクライアントが代替トークンについて認識する前に古いトークンを最終的に提示できる小さな猶予期間(数秒から1分程度)を設けてください。猶予期間が全くないと、不安定な接続でユーザーがロックアウトされます。逆にウィンドウが長すぎると攻撃対象の範囲が広がります。
各トークンの保存場所は、その有効期限から直接決まります:
- 問題 アクセストークンは、 TTLを短く(数日ではなく数分単位)設定し、それらを メモリ内にのみ保持して、 セッション中だけにします。
- 永続化するトークンは、 refresh トークン KeychainまたはKeystoreを利用した暗号化ストレージに保存し、上記と同様にローテーションしてください。
- パスワード変更時、ログアウト時、デバイス紛失時には、 サーバー側で リフレッシュトークンを無効化してください。クライアントがローカルデータを削除しても、失効にはなりません。
- 軽量な トークンバインディング 戦略(デバイスキーのアテステーションやアプリインスタンスID)を、脅威モデル上必要なら検討してください。
Capacitor アプリのトークン処理に関するセキュリティ修正をリリースする際、 Capgo Live Updates ストアのポリシーを遵守している場合、対象となるWebレイヤーの変更を迅速に配信できます。ネイティブストレージの変更には、ストアへのビルド提出が必要です。Capgoの セキュリティ概要 本記事では、本番環境のアプリがトランスポート、ストレージ、および更新制御をどのように組み合わせるかをまとめます。
ストレージオプションの比較
| メソッド | セキュリティ | Capacitor との相性 |
|---|---|---|
| インメモリJS変数 | 実行中は高いが、キル時に消失 | セッション中のアクセストークンのみならOK |
localStorage / 設定 |
低 | 設定のみ。シークレットは不可 |
| iOS Keychain / Android Keystore | 高 | ネイティブの code または Capgo プラグイン経由 |
| DataStore + Tink (Android ネイティブ) | 高 | カスタム Capacitor プラグインまたはネイティブ認証 SDK |
| JS でのカスタム暗号化 | 変数 | 避ける: JS 内のキーは抽出可能 |
最後の行で注目すべき点: JavaScript 暗号の失敗モードはアルゴリズムの弱さではなく、配置です。JS ランタイムが読み取れるキーは、同じランタイム内の他のコードによって外部に持ち出され得るため、どのライブラリを選んでも、そのレイヤーから「高セキュリティ」に構造的に到達することはできません。
EncryptedSharedPreferencesからの移行チェックリスト
- どのキーがシークレットで、どれが公開設定かを洗い出します。
- DataStore + を実装
AeadSerializerKeystore バックの Tink キーセットを使用します。 - 移行には
SharedPreferencesMigrationレガシーの EncryptedSharedPreferences インスタンスを開くプロデューサーを使用します(プレーンなSharedPreferences)、その後、読み取りが成功したらレガシーファイルを削除します。 - ネイティブの変更をストアリリースで配信し、実機でバックアップと復元をテストします。
- 削除
androidx.security.crypto参照されていない場合の依存関係。
よくある質問
セキュアなトークンストレージを実装するには?
iOSでは、バックグラウンドリフレッシュの要件に合わせたアクセシビリティ設定で、リフレッシュトークンをKeychainに書き込みます。Androidでは、Jetpack DataStoreと AeadSerializer を使用し、非推奨のEncryptedSharedPreferencesの代わりにします。アクセストークンはメモリ内にのみ保持し、HTTPSを使用します。 証明書ピンニング 脅威モデルで必要とされる場合に追加します。
モバイルアプリでリフレッシュトークンを安全に管理・ローテーションするには?
リフレッシュトークンはKeychainまたはKeystoreバックのストレージにのみ保存します。使い捨てのリフレッシュトークンを発行し、各交換で新しいリフレッシュトークンを返し、古いものを無効にします。短い再利用検出ウィンドウを維持し、リプレイされたトークンでファミリー全体を失効させ、オフラインや競合するクライアントには小さなグレース期間を設けます。
iOSとAndroidで、アクセストークンとリフレッシュトークンの安全な保存方法は同じですか?
分割の考え方は両プラットフォームで同じです。有効期間の短いアクセストークンはメモリに、リフレッシュトークンはプラットフォームが保護する暗号化ストレージに保存します。APIは異なります(Keychain 対 Keystore + DataStore)が、どちらも秘密情報にWebViewのlocalStorage、UserDefaults、平文のSharedPreferencesを使ってはいけません。
Keychainに保存する前にトークンを暗号化すべきですか?
Keychainのアクセシビリティが脅威モデルと合っていれば、標準的なベアラートークンに追加のレイヤーは通常不要です。規制やキーローテーションポリシー上必要な場合にエンベロープ暗号化を追加してください。
Capacitor プラグイン内のSQLCipherはKeystoreの代わりになりますか?
SQLCipherはデータベースファイルを保護しますが、データベースを復号するにはKeystoreまたはKeychainに強力なキーが必要です。暗号化パスフレーズの扱いも、セキュアストレージ設計の一部として考えてください。
難読化して Capacitor Preferences にトークンを保存できますか?
難読化は暗号化ではありません。PreferencesはUserDefaultsとSharedPreferencesに対応しますが、ハードウェアによる保護はありません。
モバイルアプリはリフレッシュトークンを保存すべきですか?
はい、ただしKeychainまたはKeystoreバックの暗号化ストレージに保存し、サーバー側のローテーションと失効を組み合わせる場合に限ります。保存しないと起動のたびに再認証が必要になり、ユーザーはより弱いログイン要件を求めてこれを回避しようとします。決して許容されないのは、リフレッシュトークンをPreferences、localStorage、その他の平文ファイルに保存することです。
モダンなアプリでアクセストークンを保存する最も安全な方法は何ですか?
アクセストークンを永続化しないこと。セッション中はメモリに保持し、起動のたびにリフレッシュトークンから再構築する。永続化するのはリフレッシュトークンだけで、iOS Keychain または Android Keystore による暗号化ストレージに保存し、ローテーションと失効機能でデバイスが侵害された場合の被害を最小限に抑える。
「セキュアなトークンストレージ:モバイル開発者のためのベストプラクティス」の続きを読む
使用している場合 セキュアなトークン保存: モバイル開発者向けベストプラクティス セキュリティとコンプライアンスを計画するには、これを以下と連携させてください 暗号化 暗号化の実装詳細について コンプライアンス コンプライアンスの実装詳細について Capgo セキュリティスキャナー Capgo Security Scanner のプロダクトワークフローについて Capgo のセキュリティ Capgo Security のプロダクトワークフロー向け、および Capgo トラストセンター Capgo Trust Center のプロダクトワークフロー向け。