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
.envDateiencapacitor.config.tsAlle liefern sie innerhalb der App-Bundle und jeder kann ein IPA oder APK entpacken. - oder
@capacitor/preferencesorlocalStorage. 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.xmlWenn sie nach der Wiederherstellung ohnehin nicht entschlüsselt werden kann. Es spart Ihren Benutzern einen verwirrenden Reset. - On iOS, Dateien in
Documentsgehen 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_sequencewird übersprungen. SQLite aktualisiert es automatisch, wenn Zeilen mit expliziten IDs eingefügt werden, sodassAUTOINCREMENTweitergeht von der höchsten ID.- Virtual Tabellen (FTS) erstellen interne Schatten Tabellen. Erstellen Sie FTS Tabellen erneut aus ihren
CREATE VIRTUAL TABLEStatment 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 5000anstatt einer einzelnenSELECT *. - 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)
ITSAppUsesNonExemptEncryptioninInfo.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
- Fügen Sie SQLCipher hinzu:
sqlcipher-androidim Android-App-Modul, demSQLCipherUntermodul auf iOS. - Erstelle bei der ersten Ausführung eine zufällige 256-Bit-Schlüssel.
- Speichern Sie es mit
@capgo/capacitor-native-biometric(setData, optional mitaccessControl). - Mit öffnen
encrypted: true, then read once to verify the key. - Plan for key loss: reset and resync.
- Migrieren Sie bestehende plaintext-Datenbanken einmal und rotieren Sie die Schlüssel mit
PRAGMA rekeyWenn 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.