Per criptare un database SQLite in Capacitor, utilizza un plugin costruito con SQLCipher, apri il database con una chiave di cifratura e conserva quella chiave nella Keychain di iOS o nel Keystore di Android al posto del tuo code. Questa guida lo fa con @capgo/capacitor-fast-sql su Capacitor 8: configurazione di setup per entrambe le piattaforme, generazione e memorizzazione della chiave, cifratura di un database che già contiene dati degli utenti, rotazione delle chiavi e i casi di fallimento che devi gestire.
Cosa SQLCipher protegge e cosa non protegge
SQLCipher è un'estensione open source di SQLite che cifra ogni pagina del file di database con AES-256. Senza la chiave il file è byte casuali. Copre anche i file WAL e di registro.
It protegge contro:
- qualcuno che copia i file dell'applicazione da un dispositivo jailbreakato o radicato, un backup non crittografato o un telefono perso con un codice di accesso debole
- strumenti forensi che leggono i contenitori dell'app.
- altri app che leggono il file attraverso una directory condivisa configurata male
It non protegge contro:
- code che esegue all'interno della tua app (una dipendenza maliziosa, un bug XSS nel WebView) che può chiamare il layer dei dati
- un attaccante che può estrarre la chiave, il che è il motivo per cui la gestione delle chiavi è più importante del cifrario
- i dati che anche conservi in chiaro altrove: i log.
localStoragecaché, eventi di analisi.
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 archiviazione di database sicura.
Prerequisites
Fast SQL installato e configurato come in Come utilizzare SQLite in un'app CapacitorLa crittografia è facoltativa, quindi l'installazione base non include SQLCipher, incluso l'impostazione di rete locale iOS e l'impostazione di localhost Android cleartext.
Passo 1: aggiungi SQLCipher alle costruzioni native
Android
Aggiungi la dipendenza al app-level android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}
Il plugin README specifica la versione 4.13.0. Le nuove rilasci della versione 4 funzionano sempre che la versione maggiore rimane 4. Senza questa dipendenza, encrypted: true fallisce con un chiaro errore (“libreria SQLCipher nativa non trovata”) invece di bloccarsi.
SQLCipher fornisce una versione nativa .so SQLCipher invia file nativi per ogni ABI. Si aspetta che l'APK aumenti di pochi megabyte. I bundle dell'app consegnano solo l'ABI necessario per ogni dispositivo.
IOS
Cambia il plugin per la sua sottospecie SQLCipher in ios/App/Podfile, dopo bunx cap sync ha scritto l'ingresso predefinito:
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
Poi:
cd ios/App && pod install
bunx cap sync regenera la sezione Capacitor del file Podfile, quindi controlla questa riga dopo ogni sincronizzazione. Senza la sottospecie il plugin costruisce contro il SQLite di sistema, e aprire con encrypted: true ritorna Encryption is not available in this build.
Web
Non c'è SQLCipher sul web. Fast SQL's encrypted L'opzione è disponibile solo per iOS e Android. Se il tuo app viene anche distribuita come PWA, seleziona Capacitor.isNativePlatform() e non persisti le tabelle sensitive nella build del browser.
Passo 2: generare una chiave
Usa il Web Crypto API, disponibile nei WebViews di iOS e Android:
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('');
}
Ciò fornisce una stringa di 64 caratteri in formato esadecimale. SQLCipher tratta la stringa come una passphrase e deriva la chiave effettiva con PBKDF2-HMAC-SHA512 (256.000 iterazioni di default in SQLCipher 4). Questa derivazione viene eseguita una volta per apertura e costa una frazione di secondo notevole su dispositivi più vecchi, il che è normale.
Non utilizzare una password utente direttamente come chiave a meno che non si desideri che il database sia bloccato a quella password. Gli utenti dimenticano le password, le cambiano su un altro dispositivo e ne selezionano di deboli. Una chiave dispositivo casuale più un storage sicuro è il design usuale. Se si necessita di accesso controllato dagli utenti, controllare l'accesso alla chiave (con biometria) al posto.
Passo 3: memorizza la chiave nella Keychain o Keystore
@capgo/capacitor-native-biometric memorizza stringhe arbitrarie crittografate in stato di riposo con la Keychain di iOS e Keystore di Android. setData e getData (non plugin 8.6.0 e in poi) non sollecitare l'utente. Aggiungi accessControl richiedere l'autenticazione con Face ID, Touch ID o impronta digitale prima di poter leggere la chiave.
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 });
}
Facoltativo: richiedi biometria per sbloccare il database
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 resiste a una nuova impronta digitale o alla registrazione del volto. 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 autenticazione biometrica negli app Capacitor.
Dove non mettere la chiave
- Nel tuo JavaScript
.envfile, ocapacitor.config.tsTutti di essi vengono spediti all'interno del pacchetto dell'app e chiunque può scompattare un IPA o un APK. - In
@capacitor/preferencesolocalStorageSono testo in chiaro. - 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.
Passo 4: apri il database crittografato
// 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 Non si verificano le chiavi. SQLCipher non verifica la chiave al momento dell'apertura. La prima lettura reale fallisce con file is not a database if the key is wrong, so do a cheap read immediately and handle the error in one place instead of in the middle of a screen.
To check that encryption is active, run PRAGMA cipher_version su di un collegamento. Restituisce la versione di SQLCipher e un risultato vuoto per SQLite normale.
Passo 5: gestire una chiave persa o sbagliata
Una chiave può andare persa nell'uso normale:
- Lo utente ripristina un backup su un nuovo telefono. Il file di database torna, ma gli elementi di Keychain creati come dispositivi unici e le chiavi di Android Keystore non si spostano su nuovi hardware.
- Android Auto Backup ripristina i file dell'applicazione, ma la chiave di Keystore utilizzata per cifrare il segreto archiviato non è stata ripristinata.
- Lo utente cambia l'iscrizione biometrica e si è utilizzato
BIOMETRY_CURRENT_SET.
La ripristinazione è sempre la stessa: i dati sono andati perduti localmente, quindi cancella il file e inizia da capo, poi sincronizza nuovamente dal tuo 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();
}
Chiamalo quando verifyKey fallisce con file is not a database, resettare dbPromise, poi riapri. Viene generata una nuova chiave e viene creata una database vuota.
Due a due impostazioni correlate da verificare:
- On Android, consider escludere il database da Auto Backup in
data_extraction_rules.xmlse non può essere decrittografato dopo il ripristino comunque. Salva ai tuoi utenti un reset confuso. - On iOS, i file in
Documentsvanno a iCloud backups. Crittografati, quindi leggibili solo con la chiave, ma escluderli mantiene i backup piccoli se i dati possono essere sincronizzati nuovamente.
Crittografare un database di testo esistente
Se il tuo app ha già distribuito un database non crittografato, non puoi attivare encrypted: true su lo stesso file. SQLCipher tenterebbe di decrittografare pagine di testo piane e fallirebbe. Copia i dati in un nuovo database crittografato una volta:
// 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;
}
Eseguilo una volta, protetto da un flag memorizzato dopo il successo, e mostra uno stato di avanzamento perché legge ogni riga. Note:
- Le tabelle vengono create e riempite per primi, gli indici, i trigger e le viste dopo. Costruire gli indici una volta all'fine è più veloce, e i trigger non si attivano mentre le righe esistenti vengono copiate.
sqlite_sequenceè saltato. SQLite lo aggiorna automaticamente quando vengono inseriti righe con id espliciti, quindiAUTOINCREMENTcontinua dall'id più alto.- Le tabelle virtuali (FTS) creano tabelle di ombra interne. Ricrea le tabelle FTS dalle loro
CREATE VIRTUAL TABLELa dichiarazione e la ricostruzione dell'indice al posto delle tabelle di ombra. - Per le tabelle molto grandi, sfoglia con
WHERE rowid > ? ORDER BY rowid LIMIT 5000al posto di una singolaSELECT *. - La cancellazione del file in chiaro è l'obiettivo dell'esercizio. Il disco flash cancellato può ancora contenere vecchi blocchi, ma il file non è più raggiungibile attraverso il sistema di file.
La rotazione della chiave
SQLCipher ri-cifra l'intero database con PRAGMA rekeyCon Fast SQL lo si esegue come dichiarazione su una connessione crittata aperta:
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 });
}
L'ordine conta. Se l'app viene uccisa tra le due righe, il database utilizza la nuova chiave mentre la Keychain ancora tiene la vecchia. Per evitare ciò, scrivi la nuova chiave con un secondo nome prima, ri-cifra, poi promuovila e cancella l'ingresso vecchio, e al riavvio prova la chiave pendente se quella principale fallisce. PRAGMA non accetta parametri vincolati, il che è il motivo per cui la chiave è in linea. Inserisci solo valori inline generati, mai l'input dell'utente. Testa la rotazione su entrambe le piattaforme con una copia di dati di dimensioni di produzione, poiché rekey riscrive ogni pagina.
La crittografia con altri plugin
| Plugin | How funziona la crittografia | Gestione delle chiavi |
|---|---|---|
@capgo/capacitor-fast-sql |
Abilita SQLCipher su iOS e Android | Passa la chiave connect() |
@capacitor-community/sqlite |
SQLCipher sempre integrato, abilitato in capacitor.config |
Il plugin memorizza una passphrase per app setEncryptionSecret(), modalità secret, encryption (crittografare un file esistente), newsecret |
@capgo/capacitor-data-storage-sqlite |
SQLCipher per il suo archivio di valori chiave | Passphrase impostata in capacitor.config, quindi si invia con l'app |
La plugin della community encryption il modo può cifrare una database esistente in loco, il che è comodo se si è già su di esso. Con una passphrase in capacitor.config (data-storage-sqlite), trattare la crittografia come l'obfuscation: il segreto è nel bundle. Per valori sensibili veramente, preferisci Fast SQL con una chiave da un storage sicuro. Vedi opzioni di archiviazione chiave-valore in Capacitor per quel trade-off.
Note di conformità
- Conformità all'esportazione dell'App Store. Con l'invio di SQLCipher, il tuo app contiene l'encryption. App Store Connect chiede informazioni su di esso per ogni build (o lo imposti)
ITSAppUsesNonExemptEncryptioninInfo.plist). La crittografia dei dati dell'utente sul dispositivo ricade spesso sotto le eccezioni, ma la risposta dipende dal tuo app e dai paesi in cui lo distribuisci. Controlla la guida di Apple e ottieni consiglio se sei incerto. La nostra panoramica sulla crittografia dell'app copre la visione d'insieme più ampia. - La licenza SQLCipher. La versione comunitaria utilizza una licenza BSD-stile che richiede la sua dichiarazione di copyright nella tua app. Aggiungila alla schermata dei tuoi licenze open source.
- Sicurezza dei dati Google Play. Puoi dichiarare i dati come crittografati in stato di riposo. Assicurati che sia vero per ogni copia, comprese cache e log.
Troubleshooting
file is not a database La crittografia dei dati si attiva La chiave è sbagliata, o il file è in testo puro e l'hai aperto con encrypted: trueVerifica che la chiave nel storage sicuro sia quella utilizzata alla creazione, quindi utilizza la migrazione sopra per i file in testo puro.
Encryption is not available in this build su iOS. Il file Pod ancora utilizza la sottospecie predefinita. Riagggiungi la CapgoCapacitorFastSql/SQLCipher riga dopo cap sync e esegui pod install.
SQLCipher native library not found su Android. La sqlcipher-android La dipendenza manca nel modulo dell'applicazione, o l'hai aggiunta al plugin o alla root. build.gradle manca
L'apertura del database è lenta. Deriva la chiave ad ogni apertura. Apri una volta al boot e riutilizza la connessione al posto di connettersi per ogni query.
Il database si apre su un dispositivo ma non dopo il ripristino. La chiave non è stata trasferita con il backup. Gestiscila con il flusso di reset e sincronizzala nuovamente dal server.
l'hai
- Aggiungi SQLCipher:
sqlcipher-androidnella cartella del modulo dell'app AndroidSQLCiphersottospecie su iOS. - Genera una chiave casuale da 256 bit alla prima avviatura.
- Salvalo con
@capgo/capacitor-native-biometric(setData, opzionalmente conaccessControl). - Apre con
encrypted: true, poi leggilo una volta per verificare la chiave. - Plan for key loss: reset and resync.
- Migra database di testo in chiaro una volta sola, e ruota le chiavi con
PRAGMA rekeyse la tua politica lo richiede.
Aggiungere cambiamenti nativi come SQLCipher richiede una nuova costruzione del database. Capgo Costruisci può produrre binari iOS e Android firmati senza un Mac locale, e il Documentazione SQL veloce elenco tutte le opzioni di connessione.