Zum Hauptinhalt springen

So verschlüsseln Sie eine SQLite-Datenbank in Capacitor

Verschlüsseln Sie eine SQLite-Datenbank in Capacitor mit SQLCipher: Einrichten auf iOS und Android, den Schlüssel sicher speichern, bestehende Daten migrieren und Schlüssel rotieren.

Artikelcredits

Martin Donadieu

Autor

Valeria

Rezensent

Jordan

Redakteur

Wie man eine SQLite-Datenbank in Capacitor verschlüsselt

Um eine SQLite-Datenbank in Capacitor zu verschlüsseln, verwenden Sie einen Plugin, der mit SQLCipher erstellt wurde, öffnen Sie die Datenbank mit einem Verschlüsselungsschlüssel und speichern Sie diesen Schlüssel in der iOS-Schlüsselkette oder dem Android-Keystore anstatt in Ihrem code. Diese Anleitung macht es mit @capgo/capacitor-fast-sql auf Capacitor 8: Einrichtung für beide Plattformen, Schlüsselgenerierung und -speicherung, Verschlüsselung einer Datenbank, die bereits Benutzerdaten enthält, Schlüsselrotation und die Fehlerfälle, die Sie behandeln müssen.

Was SQLCipher schützt und was es nicht

SQLCipher ist eine Open-Source-Erweiterung von SQLite, die jede Seite der Datenbankdatei mit AES-256 verschlüsselt. Ohne die Schlüssel sind die Dateien nur zufällige Bytes. Es schützt auch die WAL- und Journal-Dateien.

Es schützt gegen:

  • jemanden, der die App-Dateien von einem jailbreakten oder rooteten Gerät kopiert, eine unverschlüsselte Sicherung oder ein verlorenes Telefon mit einem schwachen Passwort
  • forensische Werkzeuge, die App-Container lesen
  • andere Apps, die die Datei über eine falsch konfigurierte gemeinsame Verzeichnis lesen

Es schützt nicht gegen:

  • ein code-Befehl, der innerhalb Ihrer App ausgeführt wird (ein schädlicher Abhängigkeit, ein XSS-Bug im WebView), der Ihre Datenlayer aufrufen kann
  • einen Angreifer, der die Schlüssel extrahieren kann, weshalb die Schlüsselverwaltung wichtiger ist als der Verschlüsselungsalgorithmus
  • Daten, die Sie auch an anderer Stelle in Klartext speichern. localStorage, Caches, Analysevente.

iOS and Android already encrypt app storage when the device is locked. SQLCipher adds a layer that does not depend on the device passcode and that survives backups. It is worth it for health, finance, and anything regulated. For a broader view of storage threats, see sichere Datenbank-Speicherung.

Voraussetzungen

Fast SQL installiert und konfiguriert wie in How to use SQLite in a Capacitor app, einschließlich der iOS-Netzwerkfunktionen und der Android-localhost-Cleartext-Einstellungen. Die Verschlüsselung ist optional, daher ist die Basisinstallation nicht mit SQLCipher ausgestattet.

Schritt 1: Fügen Sie SQLCipher zu den nativen Builds hinzu

Android

Fügen Sie die Abhängigkeit zur app-level android/app/build.gradle:

dependencies {
    implementation 'net.zetetic:sqlcipher-android:4.13.0'
}

Die Plugin-README legt 4.13.0 fest. Neuere 4.x-Versionen funktionieren, solange die Hauptversion 4 bleibt. Ohne diese Abhängigkeit, encrypted: true fehlschlägt mit einer klaren Fehlermeldung (“SQLCipher native Bibliothek nicht gefunden”) anstatt zu abstürzen.

SQLCipher liefert native .so SQLCipher liefert native Dateien für jeden ABI. Erwartet werden einige Megabyte mehr für die APK. App-Bundles liefern nur den ABI, den jedes Gerät benötigt.

iOS

Stellen Sie den Plugin auf seine SQLCipher-Unterpunkt um ios/App/Podfile, nachdem bunx cap sync hat die Standardeinträge geschrieben:

pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'

Dann:

cd ios/App && pod install

bunx cap sync regeneriert die Capacitor Abschnitt der Podfile, also überprüfen Sie diese Zeile nach jedem Synchronisieren. Ohne den Unterpunkt baut das Plugin gegen das System-SQLite, und das Öffnen mit encrypted: true zurückgibt Encryption is not available in this build.

Website

Es gibt kein SQLCipher im Web. Schnelles SQLs encrypted Option ist nur für iOS und Android verfügbar. Wenn Ihre App auch als PWA verteilt wird, müssen Sie auf Capacitor.isNativePlatform() und persistente sensible Tabellen in der Browser-Build nicht speichern.

Schritt 2: Erstellen Sie eine Schlüssel

Verwenden Sie das Web-Crypto-API, das in den iOS- und Android-WebViews verfügbar ist:

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('');
}

Dies gibt einen 64-stelligen hexadezimalen String. SQLCipher behandelt den String als Passwort und berechnet den tatsächlichen Schlüssel mit PBKDF2-HMAC-SHA512 (256.000 Iterationen standardmäßig in SQLCipher 4). Diese Berechnung läuft einmal pro Öffnen und kostet einen merklichen Bruchteil einer Sekunde auf älteren Smartphones, was normal ist.

Verwenden Sie kein Benutzerpasswort direkt als Schlüssel, wenn Sie nicht wollen, dass die Datenbank auf dieses Passwort gesperrt ist. Benutzer vergessen ihre Passwörter, ändern sie auf einem anderen Gerät und wählen schwache Passwörter. Ein zufälliger Geräteschlüssel plus sichere Speicherung ist die übliche Konfiguration. Wenn Sie eine Benutzerzugriff benötigen, steuern Sie den Zugriff auf den Schlüssel (mit Biometrik) an.

Schritt 3: Speichern Sie den Schlüssel im Keychain oder Keystore

@capgo/capacitor-native-biometric speichert beliebige Zeichenfolgen verschlüsselt am Speicherplatz mit dem iOS-Keychain und dem Android-Keystore. setData und getData (für Plugins ab Version 8.6.0) promptiere den Benutzer nicht an. accessControl Optional: Erzwingen Sie Biometrik, um die Datenbank zu entsperren

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 });
}

überlebt eine neue Fingerabdruck- oder Gesichtserkennung.

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 überlebt einen neuen Fingerabdruck oder eine Gesichtserkennung. BIOMETRY_CURRENT_SET invalidates the key when enrollment changes, which is stricter but means the local database becomes unreadable when the user adds a fingerprint. Only choose it if the data can be resynced from your server. More on prompts and fallbacks in biometrische Authentifizierung in Capacitor-Apps.

Wo nicht das Schlüssel zu speichern

  • In Ihrem JavaScript-Code .env Dateien capacitor.config.tsAlle liefern sie innerhalb der App-Bundle und jeder kann ein IPA oder APK entpacken.
  • oder @capacitor/preferences or localStorage. Diese sind plaintext.
  • Fetched from your API on every launch without any local copy. The app would not open offline, which defeats the purpose of a local database. Fetching it once after login and storing it in the Keychain or Keystore is fine.

Schritt 4: Öffnen Sie die verschlüsselte Datenbank

// 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 In Ihrem App-Code oder in Ihren Dateien. Alle davon liegen im App-Bundle und können von jedem entpackt werden. file is not a database In Ihrem App-Code oder in Ihren Dateien. Alle davon liegen im App-Bundle und können von jedem entpackt werden.

Schritt 4: Öffnen Sie die verschlüsselte Datenbank PRAGMA cipher_version am Verbindung. Es gibt die SQLCipher-Version zurück und eine leere Ergebnisliste für plain SQLite.

Schritt 5: Verlust oder falsche Schlüssel handeln

A key can go missing in normal use:

  • Der Benutzer restauriert einen Backup auf einem neuen Telefon. Das Datenbankdatei kommt zurück, aber die Keychain-Elemente, die als Geräte-schließlich erstellt wurden, und die Android-Keystore-Schlüssel werden nicht auf das neue Hardware übertragen.
  • Android Auto Backup restores the app’s files but the Keystore key used to encrypt the stored secret was not restored.
  • Der Benutzer ändert die biometrische Registrierung und Sie haben BIOMETRY_CURRENT_SET.

Die Wiederherstellung ist immer gleich: Die Daten sind lokal verloren, also löschen Sie die Datei und beginnen Sie von vorne, dann synchronisieren Sie sich mit Ihrem Backend.

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();
}

Rufen Sie es, wenn verifyKey mit einem Fehler file is not a database, zurücksetzen dbPromiseÖffne das Projekt erneut. Ein neuer Schlüssel wird generiert und eine leere Datenbank wird erstellt.

Schritt 5: Verarbeiten eines verlorenen oder falschen Schlüssels

  • On Android, berücksichtigen Sie die Ausnahme der Datenbank von der Auto-Backup-Funktion in data_extraction_rules.xml Wenn sie nach der Wiederherstellung ohnehin nicht entschlüsselt werden kann. Es spart Ihren Benutzern einen verwirrenden Reset.
  • On iOS, Dateien in Documents gehen in iCloud-Backups. Verschlüsselt, also nur mit der Schlüssel lesbar, aber die Ausnahme bewahrt die Backups klein, wenn die Daten neu synchronisiert werden können.

Die Verschlüsselung einer bestehenden plaintext-Datenbank

If your app already shipped with an unencrypted database, you cannot flip encrypted: true kopieren Sie die Daten in eine neue verschlüsselte Datenbank. SQLCipher würde versuchen, unverschlüsselte Seiten zu entschlüsseln und scheitern.

// 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;
}

Laufen Sie es einmal aus, geschützt durch einen nach dem Erfolg gespeicherten Flag, und zeigen Sie einen Fortschrittszustand, da es jede Zeile liest. Anmerkungen:

  • Tables are created and filled first, indexes, triggers and views afterwards. Building indexes once at the end is faster, and triggers do not fire while existing rows are copied.
  • sqlite_sequence wird übersprungen. SQLite aktualisiert es automatisch, wenn Zeilen mit expliziten IDs eingefügt werden, sodass AUTOINCREMENT weitergeht von der höchsten ID.
  • Virtual Tabellen (FTS) erstellen interne Schatten Tabellen. Erstellen Sie FTS Tabellen erneut aus ihren CREATE VIRTUAL TABLE Statment und den Index neu erstellen anstatt die Schatten-Tabelle zu kopieren.
  • Für sehr große Tabellen, durchgehen Sie sie mit WHERE rowid > ? ORDER BY rowid LIMIT 5000 anstatt einer einzelnen SELECT *.
  • Die Löschung des plaintext-Dateis ist der Zweck der Übung. Gelöschte Flash-Speicher können noch alte Blöcke speichern, aber die Datei ist nicht mehr über das Dateisystem erreichbar.

Drehen Sie die Schlüssel

SQLCipher re-encrypts die gesamte Datenbank mit PRAGMA rekeyMit Fast SQL führen Sie es als Statement auf einer geöffneten verschlüsselten Verbindung aus:

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 });
}

Die Reihenfolge ist wichtig. Wenn das App zwischen den beiden Zeilen beendet wird, verwendet die Datenbank die neue Schlüssel, während der Keychain noch die alte hat. Um das zu vermeiden, schreiben Sie die neue Schlüssel unter einem zweiten Namen zuerst, rekeyen Sie dann, dann promotieren Sie es und löschen Sie die alte Eintrag, und beim Starten versuchen Sie den ausstehenden Schlüssel, wenn der Haupt-Schlüssel fehlschlägt. PRAGMA akzeptiert keine gebundenen Parameter, deshalb ist der Schlüssel eingebettet. Nur eingebaute Werte, die Sie generiert haben, nie Benutzer-Eingaben. Testen Sie die Rotation auf beiden Plattformen mit einer Kopie der Produktions-Größe, da rekey schreibt jede Seite neu.

Verschlüsselung mit anderen Plugins

Plugin How funktioniert die Verschlüsselung? Schlüsselverwaltung
@capgo/capacitor-fast-sql Opt-in SQLCipher auf iOS und Android Sie übergeben den Schlüssel connect()
@capacitor-community/sqlite SQLCipher ist immer eingebunden, aktiviert in capacitor.config Das Plugin speichert einen Passwort pro App mit setEncryptionSecret(), Modi secret, encryption (eine bestehende Datei verschlüsseln), newsecret
@capgo/capacitor-data-storage-sqlite SQLCipher für sein Schlüssel-Wert-Speicher Passwort gesetzt in capacitor.config, damit es im App-Container mitgeführt wird

Die Community-Plugin’s encryption Der Modus kann eine bestehende Datenbank in-place verschlüsseln, was praktisch ist, wenn Sie bereits auf ihr sind. Mit einem Passwort in capacitor.config (data-storage-sqlite), behandeln Sie die Verschlüsselung als Verwischung: Das Geheimnis befindet sich im Bundle. Für wirklich sensitive Werte bevorzugen Sie Fast SQL mit einer Schlüssel aus sicheren Speicher. Siehe key-value-Speicheroptionen in Capacitor für diese Kompromisslösung.

Zuverlässigkeitsanmerkungen

  • Exportkompabilität für den App Store. Schicken Sie SQLCipher bedeutet, dass Ihre App eine Verschlüsselung enthält. App Store Connect fragt danach für jede Build (oder Sie setzen) ITSAppUsesNonExemptEncryption in Info.plist). Die Verschlüsselung der eigenen Daten des Benutzers auf dem Gerät fällt in der Regel unter Ausnahmen, aber die Antwort hängt von Ihrer App und den Ländern ab, in denen Sie verteilen. Überprüfen Sie die Anleitung von Apple und suchen Sie nach Rat, wenn Sie unsicher sind. Unsere Übersicht über die App-Verschlüsselung umfasst das umfassendere Bild.
  • Lizenzvereinbarung für SQLCipher. Die Community-Edition verwendet eine Lizenz im Stil von BSD, die die Angabe des Copyrights in Ihrer App erfordert. Fügen Sie es Ihrem Bildschirm für Open-Source-Lizenzen hinzu.
  • Google Play-Datensicherheit. Sie können Daten als verschlüsselt am Speicher deklarieren. Stellen Sie sicher, dass dies für jede Kopie, einschließlich Caches und Protokollen, wahr ist.

Fehlerbehebung

file is not a database bei der ersten Abfrage. Falsche Schlüssel, oder das Datei ist plaintext und Sie öffneten es mit encrypted: true. Check that the key in secure storage is the one used at creation, then use the migration above for plaintext files.

Encryption is not available in this build auf iOS. Der Podfile verwendet noch die Standard-Subspezifikation. Fügen Sie die CapgoCapacitorFastSql/SQLCipher Zeile nach cap sync und führen Sie pod install.

SQLCipher native library not found auf Android. Die sqlcipher-android Die Abhängigkeit fehlt in der App-Modul oder Sie haben sie in der Plugin- oder der Root-Modul hinzugefügt. build.gradle anstatt.

Die Datenbank öffnet sich langsam. Key derivation läuft bei jedem Öffnen. Öffnen Sie es nur einmal bei Start und verwenden Sie die Verbindung stattdessen mehrmals, anstatt pro Abfrage neu zu verbinden.

Die Datenbank öffnet sich auf einem Gerät, aber nicht nach dem Wiederherstellen. Der Schlüssel ist bei der Sicherung nicht mitgegangen. Behandeln Sie ihn mit dem Reset-Flow und synchronisieren Sie sich erneut mit dem Server.

Zusammenfassung

  1. Fügen Sie SQLCipher hinzu: sqlcipher-android im Android-App-Modul, dem SQLCipher Untermodul auf iOS.
  2. Erstelle bei der ersten Ausführung eine zufällige 256-Bit-Schlüssel.
  3. Speichern Sie es mit @capgo/capacitor-native-biometric (setData, optional mit accessControl).
  4. Mit öffnen encrypted: true, then read once to verify the key.
  5. Plan for key loss: reset and resync.
  6. Migrieren Sie bestehende plaintext-Datenbanken einmal und rotieren Sie die Schlüssel mit PRAGMA rekey Wenn Ihre Richtlinie es erfordert.

Nativänderungen wie das Hinzufügen von SQLCipher erfordern eine neue Speichererstellung. Capgo Build Kann signierte iOS- und Android-Binärdateien ohne lokale Mac erstellen und die Rapid SQL-Dokumentation Listet alle Verbindungsoptionen auf.

Live-Updates für Capacitor-Apps

Wenn ein Web-Schicht-Bug live ist, schicken Sie die Reparatur über Capgo anstatt Tage zu warten, bis die App-Store-Zulassung vorliegt. Die Benutzer erhalten die Aktualisierung im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neueste von unserem Blog

Capgo gibt Ihnen die besten Einblicke, die Sie benötigen, um ein wirklich professionelles Mobiltelefon-App zu erstellen.