トークン・鍵・個人情報(PII)のためのKotlinセキュアストレージチェックリスト
トークン、鍵、個人情報(PII)を保護するために、Android Keystore、EncryptedSharedPreferences、データベース暗号化のどれを選ぶかを判断するKotlin向けセキュアストレージチェックリスト。

守りたいもの(平易に言うと)
業務アプリでのセキュアストレージは一つの目的に集約されます:誰かが端末やアプリのファイルを手に入れても、保存した情報を読み取ったり再利用したりできないこと。これはディスク上のデータ(at rest)はもちろん、バックアップ、ログ、クラッシュレポート、デバッグツール経由の漏洩も含みます。
簡単な思考実験:見知らぬ人があなたのアプリのストレージフォルダを開いたら何ができるか?多くのアプリで最も価値があるのは写真や設定ではなく、アクセスを開く小さな文字列(トークンなど)です。
端末上の保存物には、セッション用トークン(ユーザーのサインイン維持)、リフレッシュトークン、APIキー、暗号化キー、名前やメールのような個人データ(PII)、オフラインで使うキャッシュされた業務レコード(注文、チケット、顧客メモ)などが含まれます。
現実に起きる失敗パターンの例:
- 紛失や盗難された端末が解析され、トークンがコピーされてユーザーになりすます。
- ルート化デバイスやアクセシビリティの悪用でマルウェアや「ヘルパー」アプリがローカルファイルを読む。
- 自動バックアップでアプリデータが意図しない場所へ移動する。
- デバッグビルドがトークンをログ出力したりクラッシュレポートに書き込んだり、セキュリティチェックを無効にする。
このため「とりあえずSharedPreferencesに入れればいい」は、トークンやユーザーに害を及ぼす可能性がある何かには適していません。プレーンな SharedPreferences はアプリ内に付箋で秘密を書いておくようなもので、便利ですが誰かに見られたら簡単に読まれます。
実務的な出発点は、保存する各項目に名前を付けて次の2つを問うことです:それは何かをアンロックするか?それが公開されたら問題になるか?後は(Keystore、暗号化プリファレンス、暗号化データベース)のどれに入れるかが決まります。
データの分類:トークン、鍵、PII
すべての「機密データ」を同じ扱いにするのをやめると、セキュアストレージは簡単になります。まずアプリが保存するものを列挙して、漏れたら何が起きるかを考えてください。
トークンはパスワードと同じではありません。アクセストークンやリフレッシュトークンはユーザーをサインイン状態に保つため保存されますが、それでも価値の高い秘密です。パスワード自体は保存すべきではありません。ログインが必要なら、セッション維持に必要な最小限(通常はトークン)だけを保存し、パスワードチェックはサーバーに頼るべきです。
鍵は別カテゴリです。APIキー、署名鍵、暗号化鍵はシステム全体を解除できることがあり、端末から抽出されると大規模な不正利用が可能になります。アプリ外で偽装や復号に使える値は、ユーザートークンより高リスクと考えてください。
**PII(個人情報)**は人を特定し得るものすべて:メール、電話、住所、顧客メモ、身分証、健康情報などです。個別には無害に見えるフィールドも、組み合わさると敏感になります。
実務で便利な簡易ラベリング:
- セッションシークレット:アクセス/リフレッシュトークン、セッションクッキー
- アプリシークレット:APIキー、署名鍵、暗号化鍵(可能なら端末に置かない)
- ユーザーデータ(PII):プロファイル、識別子、書類、医療・金融情報
- 端末/解析ID:広告ID、デバイスID、インストールID(多くの方針で敏感扱い)
Android Keystore:使うべきケース
Android Keystoreは、平文で端末外に出してはいけない秘密を守るときに最適です。これは暗号鍵のための金庫であり、実際のデータベースではありません。
得意なこと:暗号化、復号、署名、検証に使う鍵を生成・保管すること。一般的には、トークンやオフラインデータ自体は別途暗号化し、その暗号化を解除する鍵をKeystoreが保持します。
ハードウェアバックド鍵:本当に意味すること
多くの端末でKeystoreの鍵はハードウェアで保護され、鍵操作は保護環境内で行われ、鍵素材は抽出できません。これによりアプリファイルを読むマルウェアからのリスクが下がります。
ただしハードウェアバックドがすべての端末で保証されるわけではなく、モデルやAndroidバージョンで挙動が変わります。鍵操作が失敗することを想定して設計してください。
ユーザー認証のゲート
Keystoreは鍵使用にユーザーの存在確認を要求できます。これにより生体認証や端末認証とアクセスを結びつけられます。例として、エクスポート用トークンを暗号化しておき、指紋やPINで認証があった場合のみ復号する、といった運用が可能です。
Keystoreは、鍵をエクスポート不可にしたい場合、重要操作に生体認証を要求したい場合、バックアップや同期と一緒に移動してほしくないデバイス固有の秘密を保持したい場合に適します。
落とし穴に備えてください:ロックスクリーンの変更や生体情報の変更、セキュリティイベントで鍵が無効化されることがあります。失敗を想定した回復策を実装し、無効な鍵を検出したら暗号化されたデータを消し、ユーザーに再サインインを促すなどの扱いにしてください。
EncryptedSharedPreferences:いつ十分か
EncryptedSharedPreferences は、小さなキー値の秘密が少数ある場合のデフォルトとして適しています。要するに「暗号化されたSharedPreferences」です。誰かがファイルを開いて値を読めないようにします。
内部ではマスターキーを使って値を暗号化/復号し、そのマスターキーは Android Keystore によって保護されます。つまり生の暗号鍵をアプリが平文で持たないようになっています。
頻繁に読む小さな項目(アクセス/リフレッシュトークン、セッションID、デバイスID、環境フラグ、最終同期時刻など)には通常十分です。どうしても保存しなければならない小さなユーザーデータも扱えますが、PIIのゴミ箱にしないでください。
大きな構造化データには向きません。オフラインリスト、検索、フィールドによるクエリが必要なら EncryptedSharedPreferences は遅く使いにくくなり、その時点で暗号化データベースを検討するべきです。
単純なルール:保存するキーを1画面で列挙できるなら、EncryptedSharedPreferences で十分である可能性が高いです。行やクエリが必要なら次へ進んでください。
データベース暗号化:必要なとき
数バイトや1つのトークン以上を保存するなら、データベース暗号化が重要です。業務データを端末に保持する場合、紛失端末から抽出されることを前提に保護を考えてください。
データベースはオフラインアクセス、ローカルキャッシュの性能、履歴や監査トレイル、長いメモや添付ファイルを扱うときに適しています。
よくある2つの暗号化アプローチ
フルデータベース暗号化(例えばSQLCipher風)はファイル全体をオンディスクで暗号化します。アプリはキーでファイルを開きます。どのカラムが保護されるかを覚える必要がないので考えやすいです。
アプリ層のフィールド暗号化は書き込む前に特定フィールドだけを暗号化し、読み出した後に復号します。ほとんどのレコードが非機密で一部のみ保護すれば良い場合や、ファイル形式を変えたくないときに有効です。
検索・ソートとのトレードオフ
フル暗号化はディスク上のすべてを隠しますが、データベースがアンロックされれば通常通りクエリできます。
フィールド暗号化は特定カラムを守れますが、暗号化された値での検索やソートが難しくなります。暗号化された姓でソートできず、検索は「復号後に検索する(遅い)」か「追加のインデックスを保存する(複雑さと漏洩リスク増)」のどちらかになります。
鍵管理の基本
データベースキーはハードコーディングしてはいけません。一般的なパターンはランダムなDBキーを生成し、それをKeystoreの鍵でラップ(暗号化)して保存することです。ログアウト時にラップ済みキーを削除してローカルDBを破棄するか、アプリがセッション間でオフライン動作を維持する必要がある場合は扱いを設計してください。
選び方:実務的比較
一般論で「最も安全なもの」を選ぶのではなく、アプリの使い方に合った十分に安全な選択をするのが目的です。
実際に適切な選択を導く質問:
- データはどれくらいの頻度で読まれるか(起動ごとに読むか稀か)?
- データ量はどれくらいか(数バイトか何千件か)?
- 漏れたらどうなるか(迷惑で済むか、コストや法的報告が必要か)?
- オフラインアクセス、検索、ソートが必要か?
- 準拠しなければならない要件(保存期間、監査、暗号化規則)があるか?
実用的なマッピング:
- トークン(OAuthのアクセストークンとリフレッシュトークン)は、小さく頻繁に読まれるので通常は
EncryptedSharedPreferencesが向きます。 - 鍵素材 は可能な限り
Android Keystoreに置き、端末からコピーされにくくします。 - PII やオフライン業務データ は、フィールドが数個を越えるかオフラインでの一覧・フィルタが必要ならデータベース暗号化を検討してください。
混在データは業務アプリでは普通です。実用的なパターンは、ローカルDBやファイル用のランダムなデータ暗号化キー(DEK)を生成し、そのDEKだけをKeystoreで保護された鍵でラップし、必要に応じて回転することです。
迷ったら簡単で安全な方を選んでください:保存を減らす。オフラインにPIIを置かない、鍵はKeystoreに入れる、が基本です。
ステップバイステップ:Kotlinアプリでの実装
まず端末に保存するすべての値と「なぜそこに置くのか」を書き出してください。これが「念のため」に保存するのを防ぐ最速の方法です。
コードを書く前にルールを決めてください:各項目の寿命、いつ置換するか、ログアウトが意味すること。アクセスTokenは15分で期限切れ、リフレッシュトークンはもっと長い、オフラインPIIは30日で削除、など具体的に決めます。
保守しやすい実装:
- アプリの残りの部分が直接
SharedPreferencesやKeystore、DBに触らないように単一の「SecureStorage」ラッパーを作る。 - 項目ごとに適切な場所に入れる:トークンは
EncryptedSharedPreferences、暗号化キーはAndroid Keystoreで保護、オフラインデータは暗号化DB。 - 故意に失敗を扱う:セキュアストレージが失敗したら閉じて失敗させ、プレーン保存に静かにフォールバックしない。
- データを漏らさない診断:イベントタイプやエラーコードは記録しても、トークンや鍵、個人情報は決してログに残さない。
- 削除経路をつなぐ:ログアウト、アカウント削除、「アプリデータを消去」は同じワイプルーチンに流す。
その後、バックアップからの復元、古いバージョンからのアップグレード、ロック設定の変更、機種変更など、セキュアストレージを壊す退屈なケースをテストしてください。アプリが復号できなくなっても無限ループしないようにしましょう。
最後に、どのデータをどこに置くか、保存期間、復号失敗時の挙動をチーム全員が参照できる1ページのドキュメントにまとめてください。
セキュアストレージを壊す一般的ミス
ほとんどの失敗はライブラリ選択の誤りではなく、小さな近道がいつの間にか秘密を意図しない場所にコピーしてしまうことです。
最大のレッドフラグは長持ちするリフレッシュトークンや同様のトークンがプレーンテキストでどこかに保存されていること:SharedPreferences、ファイル、一時キャッシュ、ローカルDBのカラムなど。バックアップやルートデバイス、デバッグアーティファクトに触れられるとそのトークンはパスワードより長く有効であり続け得ます。
秘密は保存より「見えること」で漏れることもあります。リクエストヘッダを丸ごとログ出力する、デバッグ中にトークンを出力する、クラッシュレポートやアナリティクスにコンテキストを添えすぎる、などは資格情報を端末外に露出します。ログは公開物として扱ってください。
鍵管理の欠落もよくある問題です。すべてを1つの鍵でやると被害範囲が広がります。鍵を回転させないと古い侵害が有効なままになります。鍵のバージョン管理、回転、旧暗号データの扱いを計画に入れてください。
「金庫の外」のパスを忘れないで
暗号化はクラウドバックアップがローカルデータをコピーするのを止めません。スクリーンショットや画面録画、デバッグビルド、エクスポート機能(CSVや共有シート)、クリップボード利用も情報を漏らします。
また暗号化は認可の欠陥を直しません。ログアウト後にPIIを表示したり、キャッシュを消さない実装はアクセス制御のバグです。UIをロックし、ログアウト時にセンシティブなキャッシュをワイプし、保護データを表示する前に権限を再確認してください。
運用上の詳細:ライフサイクル、ログアウト、エッジケース
セキュアストレージはどこに置くかだけではなく、時間経過でどう振る舞うかも含みます:アプリがスリープしたとき、ユーザーがログアウトしたとき、端末がロックされたとき。
トークンについてはライフサイクルを設計してください。アクセストークンは短寿命にし、リフレッシュトークンはパスワード同様に扱います。トークンが期限切れなら静かに更新し、リフレッシュが失敗したら(取り消し、パスワード変更、デバイス削除)無限リトライを止めてクリーンなサインインに誘導してください。サーバー側の取り消しにも対応しましょう。
生体認証は再認証に使い、すべてに使うべきではありません。PII閲覧、データエクスポート、支払い先の変更、ワンタイムキー表示など高リスク操作に対して促すのが適切で、毎回のアプリ起動で促すのは避けてください。
ログアウト時は厳格に、予測可能に:
- まずメモリ内コピーを消す(シングルトンやインターセプター、ViewModelにキャッシュされたトークン)。
- 保存されたトークンとセッション状態をワイプ(リフレッシュトークン含む)。
- 設計が許せばローカルの暗号鍵を無効化・削除する。
- オフラインPIIやキャッシュされたAPIレスポンスを削除する。
- バックグラウンドジョブを無効化して再取得を止める。
業務アプリでは複数アカウント、ワークプロファイル、バックアップ/復元、端末間移行、部分ログアウト(企業ワークスペース切替)などエッジケースが重要です。強制停止、OSアップグレード、時計のずれもテストしてください。時計ずれは期限判定を壊します。
改ざん検出はトレードオフです。デバッグフラグ、エミュレータ判定、簡単なroot検出、Play Integrityの判定などの基本チェックはカジュアルな悪用を減らしますが、熟練した攻撃者は回避可能です。改ざんシグナルはリスク入力として扱い:オフラインアクセスを制限し、再認証を要求し、イベントを記録してください。
出荷前のクイックチェックリスト
リリース前にこれを使ってください。実際の業務アプリでセキュアストレージが壊れる箇所を狙っています。
- 端末が敵対的であると仮定する。 ルート化された端末やフルデバイスイメージを持つ攻撃者が、アプリファイル、プリファレンス、ログ、スクリーンショットからトークンやPIIを読めるか?「たぶん」は避け、重要なペイロードをKeystoreで保護して暗号化を維持する。
- バックアップと端末移行を確認する。 機密ファイルをAndroid Auto Backupやクラウドバックアップ、端末間転送に含めない。復元で鍵が失われると復号できなくなるなら、復旧フローは再認証と再ダウンロードにする。
- ディスク上のプレーンテキストを探す。 一時ファイル、HTTPキャッシュ、クラッシュレポート、アナリティクスイベント、画像キャッシュにPIIやトークンがないか確認。デバッグログやJSONダンプもチェックする。
- 期限と回転を決める。 アクセストークンは短寿命に、リフレッシュトークンは保護し、サーバー側で取り消し可能にする。鍵の回転ポリシーと、トークン拒否時のアプリの挙動(消去、再認証、1回だけのリトライ)を定義する。
- 再インストールと端末変更時の挙動。 アンインストール→再インストール後にオフラインで開いたとき、Keystore鍵が消えていたらアプリは安全に失敗するべき(暗号化データをワイプし、サインイン画面を出す。部分的に読み出して状態を壊すのは避ける)。
簡単な検証として「悪い日テスト」を行ってください:ユーザーがログアウトし、パスワードを変更し、バックアップを新端末に復元し、機上でアプリを開く。結果は予測可能であるべきです:正しいユーザーだけ復号できるか、あるいはデータはワイプされてサインイン後に再取得されるか。
例:PIIをオフラインで保存する業務アプリ
通信状況が悪い地域で使われる営業アプリを想像してください。担当者は朝一回ログインし、割り当てられた顧客をオフラインで閲覧し、商談メモを取って後で同期します。ここでチェックリストが現実に効きます。
実用的な分割例:
- アクセストークン:短寿命にして
EncryptedSharedPreferencesに保存。 - リフレッシュトークン:より厳重に保護し、復号時に端末ロック(PIN/パターン/生体)を要求することを検討。
- 顧客のPII(名前、電話、住所):暗号化されたローカルデータベースに保存。
- オフラインメモと添付ファイル:暗号化データベースに保存し、エクスポートや共有時は特に注意。
ここに2つの機能を追加するとリスクは変わります。
「Remember me」を追加するとリフレッシュトークンがアカウントへ戻る主要な扉になります。パスワードと同様に扱い、必要なら端末ロックで復号させることを検討してください。
オフラインモードを追加するとセッションだけでなく顧客リスト全体を守る必要が出ます。これは通常、データベース暗号化と明確なログアウトルール(ローカルPIIをワイプ、次回ログインに必要な最小情報のみ残す、背景同期を停止)を推します。
実機でテストしてください。少なくともロック/アンロック、再インストール時、バックアップ/復元、マルチユーザーやワークプロファイルでの分離を検証してください。
次のステップ:チームで繰り返せる習慣にする
セキュアストレージは習慣にしないと機能しません。チームが従う簡潔なストレージポリシーを作ってください:どのデータをどこに置くか(Keystore、EncryptedSharedPreferences、暗号化DB)、絶対に保存してはいけないもの、ログアウト時にワイプすべきもの。
日常のデリバリに組み込みましょう:Definition of Done、コードレビュー、リリースチェックに含めます。
軽量なレビューチェックリスト:
- 保存する項目ごとにラベルがある(トークン、鍵素材、PII)。
- ストレージ選択がコードコメントで正当化されている。
- ログアウトやアカウント切替が適切なデータだけを削除する。
- エラーやログが秘密や完全なPIIを出力しない。
- ポリシーに責任者がいて最新化している。
チームがAppMaster (appmaster.io) を使ってビジネスアプリを構築し、Androidクライアント向けのKotlinソースをエクスポートする場合も、SecureStorageラッパーの方針を統一して生成コードとカスタムコードが同じルールに従うようにしてください。
小さなPOCから始める
認証トークン1つとPIIレコード1つ(例:オフラインで必要な顧客の電話番号)を保存する小さなPOCを作ってください。新規インストール、アップグレード、ログアウト、ロックスクリーンの変更、アプリデータ消去をテストし、ワイプ挙動が正しく繰り返せることを確認してから範囲を広げましょう。
よくある質問
まず保存するものとその理由を正確に列挙してください。小さなセッションシークレット(アクセス/リフレッシュトークンなど)は EncryptedSharedPreferences に置き、暗号化キーは Android Keystoreで保護し、オフラインで顧客データやPIIを扱うなら件数が増えるごとに暗号化されたデータベースを使うのが基本です。
プレーンな SharedPreferences はファイルとして保存され、デバイスのバックアップ、ルート化されたデバイスからのファイルアクセス、デバッグアーティファクトなどを通じて読み取られる可能性があります。値がトークンやPIIであれば、通常設定として扱うと容易にコピーされ再利用されてしまいます。
Android Keystore は抽出不可能にするべき暗号鍵の生成と保管に使います。自分でファイルを暗号化するのではなく、キーをKeystoreで生成・保護し、そのキーで他のデータ(トークンやDBキー)をラップして管理するのが安全です。ユーザー認証(生体認証や端末認証)を要求する設定も可能です。
「ハードウェアバックド」は、鍵操作が保護されたハードウェア内で行われ、鍵素材自体が抽出されにくいことを意味します。これによりアプリファイルを読める攻撃者からのリスクは下がりますが、すべての端末で必ず利用できるわけではないので、失敗を想定した回復フローを持ってください。
EncryptedSharedPreferences は頻繁に読み出す少量のキー値シークレット(アクセス/リフレッシュトークン、セッションID、デバイスID、短い状態情報など)には十分なことが多いです。ただし大量データや検索・フィルタが必要な構造化データには向きません。
オフラインで業務データや大量のPIIを保存する場合、検索・ソート・履歴が必要なら暗号化データベースを選んでください。これにより、紛失端末で顧客リストやメモ全体が露出するリスクを下げつつオフライン機能を維持できます。
フルデータベース暗号化はファイル全体を保護するので扱いが簡単です。フィールド単位の暗号化は特定カラムだけを守れますが、検索やソートが難しくなり、インデックスや派生データから漏れるリスクも増えます。一般的にはファイル全体の暗号化の方が運用は楽です。
乱数で生成したデータベースキー(DEK)を作り、そのDEKだけを Android Keystore によって保護された鍵でラップして保存するパターンが一般的です。キーをハードコーディングしたりアプリに埋め込んだりしないでください。ログアウト時や鍵無効化時にラップ済みキーを削除してローカルデータを破棄する設計にしましょう。
ロック画面や生体設定の変更、OSの移行や復元などでKeystore鍵が無効化されることがあります。復号に失敗したらそれを検出して、安全に暗号化バイナリやローカルDBをワイプし、ユーザーに再サインインを促す(ループやプレーンテキストへのフォールバックは避ける)実装にしてください。
暗号化を使っていても多くの漏洩は「ボールトの外側」から起こります:ログ、クラッシュレポート、アナリティクス、デバッグプリント、HTTPキャッシュ、スクリーンショット、クリップボードなどです。ログは公開されるものと扱い、トークンや完全なPIIは絶対に記録しないでください。エクスポート機能やバックアップ経路も確認しましょう。


