Capacitor で SQLite データベースを暗号化するには、SQLCipher で構築されたプラグインを使用し、暗号化キーでデータベースを開き、そのキーを code ではなく iOS Keychain または Android Keystore に保存します。このガイドでは、これを実行します @capgo/capacitor-fast-sql Capacitor 8 での実装方法:両プラットフォームのビルド設定、キーの生成と保存、既存のユーザーデータを含むデータベースの暗号化、キーローテーション、および処理が必要な失敗ケース。
SQLCipherが保護するもの、保護できないもの
SQLCipherはSQLiteのオープンソース拡張で、データベースファイルの全ページをAES-256で暗号化します。キーがなければ、ファイルはただのランダムなバイト列です。WALファイルとジャーナルファイルも保護対象です。
以下の脅威から保護します:
- 脱獄(jailbreak)やルート化された端末からのアプリファイルのコピー、暗号化されていないバックアップ、パスコードが弱いまま紛失した端末
- アプリコンテナを読み取るフォレンジックツール
- 設定を誤った共有ディレクトリ経由でファイルを読む他のアプリ
以下の脅威には対応できません:
- アプリ内で実行され、データレイヤーを呼び出せるcode(悪意のある依存関係やWebViewのXSSバグなど)
- キーを抽出できてしまう攻撃者(だからこそ、暗号方式よりもキーの保管方法が重要になります)
- 別の場所にも平文で保存しているデータ:ログ、
localStorageキャッシュ、分析イベント。
iOSとAndroidは、デバイスがロックされている間、すでにアプリのストレージを暗号化しています。SQLCipherは、デバイスのパスコードに依存せず、バックアップ後も有効な暗号化レイヤーを追加します。ヘルスケア、金融、規制対象のデータには導入する価値があります。ストレージの脅威を広く捉えるなら、SQLite以外のバックアップ、ログ、平文のキャッシュも考慮してください。
前提条件
Fast SQL をインストールし、次のように設定します Capacitor アプリでSQLiteを使う方法。iOS のローカルネットワークと Android の localhost クリアテキスト設定を含みます。暗号化はオプトイン式のため、基本インストールには SQLCipher は含まれません。
ステップ 1: ネイティブビルドに SQLCipher を追加
Androidでは
に依存関係を追加します アプリレベル android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}
プラグインのREADMEには4.13.0が指定されています。メジャーバージョンが4のままなら、新しい4.xリリースでも動作します。この依存関係がないと、 encrypted: true クラッシュする代わりに、明確なエラー(「SQLCipherネイティブライブラリが見つかりません」)で失敗します。
SQLCipherはネイティブ .so ファイルを各ABI向けに同梱しています。APKは数MB大きくなる見込みです。App Bundleでは、各デバイスに必要なABIだけが配信されます。
iOSと
プラグインをSQLCipherサブスペックに切り替えます ios/App/Podfileの後に、 bunx cap sync デフォルトのエントリが書き込まれています:
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
次に:
cd ios/App && pod install
bunx cap sync PodfileのCapacitorセクションを再生成するため、syncのたびにこの行を確認してください。サブスペックがないと、プラグインはシステムSQLiteに対してビルドされ、 encrypted: true を返します Encryption is not available in this build.
Web
Web には SQLCipher がありません。Fast SQL の encrypted オプションは iOS と Android のみです。アプリを PWA としても配布する場合、 Capacitor.isNativePlatform() で分岐し、ブラウザビルドに機密テーブルを永続化しないでください。
ステップ 2: キーを生成
iOS と Android の WebView で利用可能な Web Crypto API を使用します:
export function generateKey(): string {
const bytes = new Uint8Array(32); // 256 bits
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
これにより 64 文字の 16 進文字列が得られます。SQLCipher はこの文字列をパスフレーズとして扱い、PBKDF2-HMAC-SHA512(SQLCipher 4 ではデフォルトで 256,000 回)で実際の鍵を導出します。この導出はオープンごとに 1 回実行され、古いスマートフォンでは目に見える程度の時間がかかりますが、これは正常な動作です。
データベースをそのパスワードにロックしたい場合を除き、ユーザーのパスワードを直接鍵として使用しないでください。ユーザーはパスワードを忘れたり、別のデバイスで変更したり、弱いものを選んだりします。ランダムなデバイス鍵と安全なストレージを使用するのが一般的な設計です。ユーザーによるアクセス制御が必要な場合は、鍵へのアクセス(生体認証など)を制御してください。
ステップ 3: キーを Keychain または Keystore に保存
@capgo/capacitor-native-biometric iOS Keychain と Android Keystore を使用して、任意の文字列を暗号化して保存します。 setData と getData (プラグイン 8.6.0 以降)はユーザーにプロンプトを表示しません。追加してください: accessControl キーの読み取り前に Face ID、Touch ID、または指紋認証を必須にする。
bun add @capgo/capacitor-native-biometric
bunx cap sync
// src/db/key.ts
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
import { generateKey } from './generate-key';
const KEY_NAME = 'app.db.key';
export async function getOrCreateDbKey(): Promise<{ key: string; created: boolean }> {
const { isSaved } = await NativeBiometric.isDataSaved({ key: KEY_NAME });
if (isSaved) {
const { value } = await NativeBiometric.getData({ key: KEY_NAME });
return { key: value, created: false };
}
const key = generateKey();
await NativeBiometric.setData({ key: KEY_NAME, value: key });
return { key, created: true };
}
export async function deleteDbKey(): Promise<void> {
await NativeBiometric.deleteData({ key: KEY_NAME });
}
オプション: データベースのロック解除に生体認証を必須にする
import { AccessControl, NativeBiometric } from '@capgo/capacitor-native-biometric';
// When creating the key
await NativeBiometric.setData({
key: 'app.db.key',
value: generateKey(),
accessControl: AccessControl.BIOMETRY_ANY,
});
// When opening the database
const { value: key } = await NativeBiometric.getSecureData({
key: 'app.db.key',
reason: 'Unlock your records',
});
BIOMETRY_ANY 新しい指紋や顔の登録後も有効なまま。 BIOMETRY_CURRENT_SET 登録情報が変わるとキーが無効になる。より厳格だが、ユーザーが指紋を追加するとローカル DB が読めなくなる。サーバーから再同期できる場合のみ選択すること。プロンプトとフォールバックの詳細は Capacitor アプリでの生体認証.
キーを保存すべきではない場所
- JavaScript の
.envファイル、またはcapacitor.config.ts。これらはすべてアプリバンドル内に同梱されており、IPA や APK を解凍すれば誰でも確認できる。 - では
@capacitor/preferencesまたはlocalStorage。これらは平文だ。 - 起動のたびにAPIから取得し、ローカルにコピーしません。オフラインではアプリが開けず、ローカルデータベースの意義が失われます。ログイン後に一度取得してKeychainやKeystoreに保存するのは問題ありません。
ステップ 4: 暗号化されたデータベースを開く
// src/db/index.ts
import { FastSQL, type SQLConnection } from '@capgo/capacitor-fast-sql';
import { getOrCreateDbKey } from './key';
import { migrate } from './migrate';
let dbPromise: Promise<SQLConnection> | null = null;
export function getDb(): Promise<SQLConnection> {
if (!dbPromise) {
dbPromise = (async () => {
const { key } = await getOrCreateDbKey();
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: key,
walMode: true,
});
await verifyKey(db);
await migrate(db);
return db;
})();
}
return dbPromise;
}
// SQLCipher only notices a wrong key when it reads the first page
async function verifyKey(db: SQLConnection): Promise<void> {
await db.query('SELECT count(*) AS n FROM sqlite_master');
}
verifyKey が重要です。SQLCipherはオープン時にキーを検証しません。最初の実際の読み取りで file is not a database がキーが間違っている場合に失敗するため、直ちに低コストの読み取りを行い、画面の途中でなく1箇所でエラーを処理してください。
暗号化が有効であることを確認するには、 PRAGMA cipher_version を接続に対して実行します。SQLCipherのバージョンを返し、プレーンなSQLiteでは空の結果を返します。
ステップ 5: キーの紛失や誤ったキーへの対応
通常の利用でキーが失われる可能性があります:
- ユーザーが新しいスマートフォンにバックアップを復元した場合。データベースファイルは戻ってきますが、デバイス専用で作成されたKeychain項目やAndroid Keystoreのキーは新しいハードウェアには移動しません。
- Android Auto Backupはアプリのファイルを復元しますが、保存されたシークレットを暗号化するために使用されたKeystoreキーは復元されません。
- ユーザーが生体認証の登録を変更し、あなたが
BIOMETRY_CURRENT_SET.
復旧の手順は常に同じです:ローカルにデータは存在しないため、ファイルを削除して最初からやり直し、バックエンドから再同期します。
import { Directory, Filesystem } from '@capacitor/filesystem';
import { FastSQL } from '@capgo/capacitor-fast-sql';
import { deleteDbKey } from './key';
export async function resetEncryptedDatabase(name = 'vault'): Promise<void> {
try {
await FastSQL.disconnect(name);
} catch {
// not connected
}
// Fast SQL stores <name>.db in Documents on iOS and in files/ on Android.
// Directory.Data maps to those two locations.
for (const suffix of ['', '-wal', '-shm']) {
await Filesystem.deleteFile({ path: `${name}.db${suffix}`, directory: Directory.Data }).catch(
() => undefined,
);
}
await deleteDbKey();
}
呼び出すタイミング verifyKey で失敗します file is not a database、リセット dbPromiseし、再度開きます。新しいキーが生成され、空のデータベースが作成されます。
確認すべき関連する2つの設定:
- Androidでは、Auto Backupからデータベースを除外することを検討してください。
data_extraction_rules.xml復元後にどうせ復号できないなら、ユーザーを混乱させるリセットを避けられます。 - iOSでは、
DocumentsiCloudバックアップに含まれます。暗号化されているため鍵がないと読めませんが、データを再同期できるなら除外してバックアップを小さく保てます。
既存の平文データベースを暗号化する
アプリがすでに未暗号化のデータベースでリリース済みなら、 encrypted: true 同じファイルのまま切り替えることはできません。SQLCipherは平文ページを復号しようとして失敗します。データを新しい暗号化データベースに一度だけコピーしてください:
// src/db/encrypt-existing.ts
import { Directory, Filesystem } from '@capacitor/filesystem';
import { FastSQL, type SQLConnection } from '@capgo/capacitor-fast-sql';
export async function encryptExistingDatabase(
plainName: string,
encryptedName: string,
key: string,
): Promise<SQLConnection> {
const plain = await FastSQL.connect({ database: plainName });
const enc = await FastSQL.connect({ database: encryptedName, encrypted: true, encryptionKey: key });
const objects = (await plain.query(
`SELECT type, name, sql FROM sqlite_master
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'
ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 WHEN 'view' THEN 2 ELSE 3 END`,
)) as { type: string; name: string; sql: string }[];
const [{ user_version }] = await plain.query('PRAGMA user_version');
const tables = objects.filter((o) => o.type === 'table');
const others = objects.filter((o) => o.type !== 'table'); // indexes, triggers, views
await enc.transaction(async (tx) => {
for (const t of tables) {
await tx.execute(t.sql);
}
for (const t of tables) {
const rows = await plain.query(`SELECT * FROM "${t.name}"`);
if (rows.length === 0) continue;
const columns = Object.keys(rows[0]);
const statement = `INSERT INTO "${t.name}" (${columns.map((c) => `"${c}"`).join(', ')})
VALUES (${columns.map(() => '?').join(', ')})`;
await tx.executeBatch(rows.map((r) => ({ statement, params: columns.map((c) => r[c]) })));
}
// Indexes and triggers after the data: faster, and triggers do not fire during the copy
for (const o of others) {
await tx.execute(o.sql);
}
await tx.execute(`PRAGMA user_version = ${Number(user_version)}`);
});
await FastSQL.disconnect(plainName);
for (const suffix of ['', '-wal', '-shm']) {
await Filesystem.deleteFile({ path: `${plainName}.db${suffix}`, directory: Directory.Data }).catch(
() => undefined,
);
}
return enc;
}
成功後に保存するフラグで制御して一度だけ実行し、全行を読むため進捗表示を出してください。注意点:
- テーブルを先に作成してデータを投入し、インデックス、トリガー、ビューは後から作成します。最後にまとめてインデックスを構築する方が速く、既存行のコピー中にトリガーは発火しません。
sqlite_sequenceはスキップされます。明示的なIDを持つ行が挿入されるとSQLiteが自動的に更新するため、AUTOINCREMENT最大のIDから続きます。- 仮想テーブル(FTS)は内部にシャドウテーブルを作成します。FTSテーブルは元の定義から再作成してください。
CREATE VIRTUAL TABLEステートメントを実行してインデックスを再構築し、シャドウテーブルをコピーする代わりにします。 - 非常に大きなテーブルでは、
WHERE rowid > ? ORDER BY rowid LIMIT 5000を単一のSELECT *. - 平文ファイルを削除することが本質です。削除されたフラッシュストレージには古いブロックが残る可能性がありますが、ファイルシステムからはもうアクセスできません。
キーのローテーション
SQLCipher は PRAGMA rekeyでデータベース全体を再暗号化します。Fast SQL では、開かれた暗号化接続に対してステートメントとして実行します:
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
import { getDb } from './index';
import { generateKey } from './generate-key';
export async function rotateKey(): Promise<void> {
const db = await getDb();
const newKey = generateKey(); // hex only, safe to inline
await db.execute(`PRAGMA rekey = '${newKey}'`);
await NativeBiometric.setData({ key: 'app.db.key', value: newKey });
}
順序が重要です。2行の間にアプリが強制終了されると、データベースは新しいキーを使用しているのに、Keychainにはまだ古いキーが残ったままになります。これを避けるため、まず新しいキーを別名で書き込み、rekey を実行し、その後昇格して古いエントリを削除します。起動時は、メインのキーが失敗した場合に保留中のキーを試します。 PRAGMA はバインドパラメータを受け付けないため、キーはインラインで記述されます。インラインで記述するのは、自分が生成した値のみで、ユーザー入力は絶対に使用しないでください。 rekey はすべてのページを書き換えます。
他のプラグインでの暗号化
| プラグイン | 暗号化の仕組み | 鍵の取り扱い |
|---|---|---|
@capgo/capacitor-fast-sql |
iOS と Android での SQLCipher のオプトイン | キーはで渡します connect() |
@capacitor-community/sqlite |
SQLCipher は常にバンドルされており、で有効化されます capacitor.config |
プラグインはでアプリごとに1つのパスフレーズを保存します setEncryptionSecret()、モード secret, encryption (既存ファイルを暗号化) newsecret |
@capgo/capacitor-data-storage-sqlite |
キーバリューストアには SQLCipher | に設定されたパスフレーズ capacitor.configのため、アプリに同梱されます |
コミュニティプラグインの encryption モードは既存のデータベースをその場で暗号化できます。すでに使用している場合は便利です。パスフレーズを capacitor.config (data-storage-sqlite) では、暗号化をオブラートとみなしてください。シークレットはバンドル内にあります。本当に機密性の高い値には、セキュアストレージからのキーを使用する Fast SQL を推奨します。 Capacitor のキーバリューストレージの選択肢 このトレードオフについて。
コンプライアンスに関する注意点
- App Store のエクスポートコンプライアンス。 SQLCipher を同梱すると、アプリに暗号化が含まれることになります。App Store Connect では、各ビルドごとに確認されます(または設定します
ITSAppUsesNonExemptEncryptionにおけるInfo.plist)。デバイス上のユーザー自身のデータの暗号化は、多くの場合、免除対象となりますが、回答はアプリの内容と配布先国によって異なります。Apple のガイドラインを確認し、不明な場合は専門家の助言を得てください。当社の アプリ暗号化概要 で、より広い視点から解説しています。 - SQLCipher ライセンス。 コミュニティ版は BSD スタイルのライセンスを使用しており、アプリ内に著作権表示を記載する必要があります。オープンソースライセンス画面に追加してください。
- Google Play データ安全性。 データを「静止時暗号化済み」として宣言できます。キャッシュやログを含むすべてのコピーでそれが真実であることを確認してください。
トラブルシューティング
file is not a database 最初のクエリ時に。 キーが間違っているか、ファイルが平文であり、それを encrypted: trueで開いた可能性があります。セキュアストレージ内のキーが作成時に使用されたものと同じであることを確認し、平文ファイルには上記のマイグレーションを使用してください。
Encryption is not available in this build iOS上で。 Podfile はまだデフォルトの subspec を使用しています。 CapgoCapacitorFastSql/SQLCipher の後に cap sync さらに実行: pod install.
SQLCipher native library not found Androidの場合 この sqlcipher-android 依存関係がアプリモジュールから欠落しているか、プラグインまたはルートに追加されました build.gradle こちらを読んでください。
データベースのオープンが遅い。 キー導出が毎回オープン時に実行されます。クエリごとに接続するのではなく、起動時に一度だけオープンして接続を再利用してください。
あるデバイスではデータベースが開けるが、復元後には開けない。 キーがバックアップに含まれていません。リセットフローで処理し、サーバーから再同期してください。
まとめ
- SQLCipher を追加します:
sqlcipher-androidAndroid アプリモジュールでは、SQLCipheriOS では subspec を使用します。 - 初回起動時にランダムな 256 ビットキーを生成します。
- 以下で保存します:
@capgo/capacitor-native-biometric(setData、任意で以下も使用:accessControl). - 以下で開く:
encrypted: true、その後キーを確認するため一度読み込みます。 - キー紛失に備えて、リセットと再同期を計画してください。
- 既存の平文データベースを一度移行し、以下でキーをローテーションします:
PRAGMA rekeyポリシーで必要なら。
SQLCipherの追加などネイティブの変更には、新しいストアビルドが必要です。 Capgo のビルド ローカルにMacがなくても署名済みのiOS/Androidバイナリを生成でき、 Fast SQL ドキュメント すべての接続オプションを一覧表示します。