メインコンテンツへスキップ

Capacitor で SQLite データベースを暗号化する方法

CapacitorでSQLCipherを使用してSQLiteデータベースを暗号化: iOSとAndroidでのセットアップ、キーの安全な保存、既存データの移行、キーのローテーション。

Capgo ロゴ。Capacitor で SQLCipher を使用して SQLite データベースを暗号化するガイド

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では、 Documents iCloudバックアップに含まれます。暗号化されているため鍵がないと読めませんが、データを再同期できるなら除外してバックアップを小さく保てます。

既存の平文データベースを暗号化する

アプリがすでに未暗号化のデータベースでリリース済みなら、 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 こちらを読んでください。

データベースのオープンが遅い。 キー導出が毎回オープン時に実行されます。クエリごとに接続するのではなく、起動時に一度だけオープンして接続を再利用してください。

あるデバイスではデータベースが開けるが、復元後には開けない。 キーがバックアップに含まれていません。リセットフローで処理し、サーバーから再同期してください。

まとめ

  1. SQLCipher を追加します: sqlcipher-android Android アプリモジュールでは、 SQLCipher iOS では subspec を使用します。
  2. 初回起動時にランダムな 256 ビットキーを生成します。
  3. 以下で保存します: @capgo/capacitor-native-biometric (setData、任意で以下も使用: accessControl).
  4. 以下で開く: encrypted: true、その後キーを確認するため一度読み込みます。
  5. キー紛失に備えて、リセットと再同期を計画してください。
  6. 既存の平文データベースを一度移行し、以下でキーをローテーションします: PRAGMA rekey ポリシーで必要なら。

SQLCipherの追加などネイティブの変更には、新しいストアビルドが必要です。 Capgo のビルド ローカルにMacがなくても署名済みのiOS/Androidバイナリを生成でき、 Fast SQL ドキュメント すべての接続オプションを一覧表示します。

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。