Pour chiffrer une base de données SQLite dans Capacitor, utilisez un plugin construit avec SQLCipher, ouvrez la base de données avec une clé de chiffrement, et gardez cette clé dans le coffre-fort de l'iPhone ou le coffre-fort d'Android au lieu de la stocker dans votre code. Cette guide le fait avec @capgo/capacitor-fast-sql sur Capacitor 8 : configuration de mise en œuvre pour les deux plateformes, génération et stockage de clés, chiffrage d'une base de données qui a déjà des données utilisateur, rotation de clés, et les cas de figure de failure que vous devez gérer.
Ce que SQLCipher protège, et ce qu'il ne protège pas
SQLCipher est une extension open source de SQLite qui chiffre chaque page du fichier de base de données avec AES-256. Sans la clé, le fichier est des octets aléatoires. Il couvre également les fichiers WAL et journal.
Il protège contre :
- Quelqu'un copiant les fichiers de l'application à partir d'un appareil jailbreaké ou rooté, d'une sauvegarde non cryptée ou d'un téléphone perdu avec un mot de passe faible.
- outils de forensic qui lisent les conteneurs d'applications,
- d'autres applications qui lisent le fichier à travers un répertoire partagé mal configuré
Il ne protège pas contre :
- code exécuté à l'intérieur de votre application (une dépendance malveillante, une faille XSS dans le WebView) qui peut appeler votre couche de données
- un attaquant qui peut extraire la clé, ce qui est pourquoi la gestion de la clé compte plus que le chiffrement
- les données que vous stockez également en clair ailleurs : les journaux
localStorage, les caches, les événements d'analytique
iOS et Android chiffrer déjà le stockage d'applications lorsque l'appareil est verrouillé. SQLCipher ajoute une couche qui ne dépend pas du mot de passe de l'appareil et qui résiste aux sauvegardes. C'est utile pour la santé, les finances et tout ce qui est réglementé. Pour une vue plus large des menaces de stockage, voir stockage de base de données sécurisé.
Prerequisites
Fast SQL installé et configuré comme dans Comment utiliser SQLite dans une application Capacitor, y compris les paramètres de réseau local iOS et les paramètres localhost Android non sécurisés. L'encryption est facultatif, donc l'installation de base ne comprend pas SQLCipher.
Étape 1 : ajouter SQLCipher aux builds natifs
context
Ajoutez la dépendance au Android android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}
Le fichier README du plugin spécifie 4.13.0. Les versions 4.x plus récentes fonctionnent tant que la version majeure reste 4. Sans cette dépendance, encrypted: true échoue avec un erreur claire (“bibliothèque native SQLCipher non trouvée”) au lieu de planter.
SQLCipher embarque natif .so échoue avec un message d'erreur clair (“La bibliothèque native SQLCipher n'est pas trouvée”) au lieu de se crasher.
IOS
Switcher le plugin vers sa sous-espèce SQLCipher dans ios/App/Podfile, après bunx cap sync l'auteur a écrit l'entrée par défaut :
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
Ensuite :
cd ios/App && pod install
bunx cap sync régénère la section Capacitor du fichier Podfile, veuillez donc vérifier cette ligne après chaque synchronisation. Sans la sous-espèce, le plugin construit contre la base de données SQLite système, et l'ouverture avec encrypted: true renvoie Encryption is not available in this build.
Web
Il n'y a pas de SQLCipher sur le web. Fast SQL's encrypted option is iOS and Android only. If your app also ships as a PWA, branch on Capacitor.isNativePlatform() et n'persistez pas les tables sensibles dans la build du navigateur.
Étape 2 : générer une clé
Utilisez la Web Crypto API, disponible dans les vues Web d'iOS et d'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('');
}
Cela donne une chaîne hexadécimale de 64 caractères. SQLCipher traite la chaîne comme un mot de passe et dérive la clé réelle avec PBKDF2-HMAC-SHA512 (256 000 itérations par défaut dans SQLCipher 4). Cette dérivation s'exécute une fois par ouverture et coûte une fraction notable de seconde sur les anciens téléphones, ce qui est normal.
Do not use a user password directly as the key unless you want the database locked to that password. Users forget passwords, change them on another device, and pick weak ones. A random device key plus secure storage is the usual design. If you need user-gated access, gate access to the key (with biometrics) instead.
Étape 3 : stockez la clé dans le Keychain ou le Keystore
@capgo/capacitor-native-biometric stocke des chaînes de caractères arbitraires chiffrées en mémoire avec le Keychain iOS et le Keystore Android. setData et getData (plugin 8.6.0 et ultérieur) n'interrogez pas l'utilisateur. Ajoutez accessControl Avant de pouvoir lire la clé, nécessitez une authentification Face ID, Touch ID ou empreinte digitale.
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 });
}
Optionnel : exigez des biométriques pour déverrouiller la base de données
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 survient à une nouvelle empreinte ou une nouvelle face étant enregistrée. BIOMETRY_CURRENT_SET annule la clé lors de changements d'enregistrement, ce qui est plus strict mais signifie que la base de données locale devient inaccessible lorsque l'utilisateur ajoute une empreinte. Choisissez-l’uniquement si les données peuvent être réynchronisées à partir de votre serveur. En savoir plus sur les invitations et les retours en arrière dans l'authentification biométrique dans les applications Capacitor.
Où ne pas mettre la clé
- Dans vos fichiers JavaScript, ou
.envfichierscapacitor.config.tsTous sont embarqués dans le bundle de l'application et n'importe qui peut dézipper un IPA ou un APK. - ou
@capacitor/preferencesorlocalStorage. Ce sont des textes en clair. - Fournis par votre API à chaque lancement sans copie locale. L'application ne s'ouvrirait pas en ligne, ce qui contredit l'objectif d'une base de données locale. Fournir le clé une fois après connexion et la stocker dans la clé de chaîne ou le coffre-fort est acceptable.
Étape 4 : ouvrir la base de données chiffrée
// 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 Les choses importantes. SQLCipher ne valide pas la clé au moment de l'ouverture. La première lecture réelle échoue avec file is not a database est important. SQLCipher ne valide pas la clé au moment de l'ouverture. La première lecture réelle échoue avec
Vérifiez que l'encryption est activé en exécutant PRAGMA cipher_version sur la connexion. Il retourne la version de SQLCipher, et un résultat vide pour SQLite classique.
Étape 5 : gérer une clé perdue ou incorrecte
Une clé peut manquer dans l'utilisation normale :
- L'utilisateur restaure un sauvegarde sur un nouveau téléphone. Le fichier de base de données revient, mais les éléments de clé de la Keychain créés comme appareil uniquement et les clés de la clé de la clé Android ne passent pas à de nouveaux matériaux.
- Android Auto Backup restaure les fichiers de l'application, mais la clé de Keystore utilisée pour chiffrer le secret stocké n'a pas été restaurée.
- L'utilisateur change l'inscription biométrique et vous avez utilisé
BIOMETRY_CURRENT_SET.
La récupération est toujours la même : les données sont parties localement, donc supprimez le fichier et commencez à nouveau, puis resynchronisez-vous avec votre 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();
}
Appelez-le lorsque verifyKey échoue avec file is not a database, réinitialisez dbPromise, puis ouvrez à nouveau. Une nouvelle clé est générée et une base de données vide est créée.
Deux paramètres liés à examiner :
- Sur Android, pensez à exclure la base de données de la sauvegarde automatique.
data_extraction_rules.xmlif it cannot be decrypted after restore anyway. It saves your users a confusing reset. - On iOS, les fichiers dans
Documentsse rendent aux sauvegardes iCloud. Chiffrés, donc lisibles uniquement avec la clé, mais en excluant-les, cela garde les sauvegardes petites si les données peuvent être resynchronisées.
Chiffrer une base de données existante
Si votre application a déjà été livrée avec une base de données non chiffrée, vous ne pouvez pas basculer encrypted: true sur le même fichier. SQLCipher essaierait de déchiffrer des pages de texte en clair et échouerait. Copiez les données dans une nouvelle base de données chiffrée une fois :
// 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;
}
Exécutez-l’une fois, protégé par un drapeau stocké après succès, et montrez un état de progression car il lit chaque ligne. Notes :
- Les tables sont créées et remplies en premier, les index, les déclencheurs et les vues ensuite. Construire les index une fois à la fin est plus rapide, et les déclencheurs ne se déclenchent pas pendant que les lignes existantes sont copiées.
sqlite_sequenceLorsque des lignes avec des identifiants explicites sont insérées, SQLite la met à jour automatiquement.AUTOINCREMENTcontinue à partir de l'ID le plus élevé.- Tables virtuelles (FTS) créent des tables d'ombre internes. Recréez les tables FTS à partir de leurs
CREATE VIRTUAL TABLELa déclaration et la reconstruction de l'index au lieu de copier les tables d'ombre. - Pour les tables très volumineuses, naviguez par page avec
WHERE rowid > ? ORDER BY rowid LIMIT 5000au lieu d'une seuleSELECT *. - Supprimer le fichier de texte en clair est le point de l'exercice. Les stockages flash supprimés peuvent encore conserver des blocs anciens, mais le fichier n'est plus accessible à travers le système de fichiers.
Rotation de la clé
SQLCipher ré-encrypte la base de données entière avec PRAGMA rekey. Avec Fast SQL, exécutez-l’en tant que déclaration sur une connexion chiffrée ouverte :
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'ordre compte. Si l'application est tuée entre les deux lignes, la base de données utilise la nouvelle clé tandis que la clé de la clé a toujours la précédente. Pour éviter cela, écrivez la nouvelle clé sous un deuxième nom en premier, réactivez-l’ensuite, promouvez-l’et supprimez l'entrée ancienne, et à l'ouverture, essayez la clé en attente si la principale échoue. PRAGMA ne prend pas en charge les paramètres liés, ce qui est pourquoi la clé est insérée. N'insérez que des valeurs générées par vous, jamais des saisies utilisateur. Testez la rotation sur les deux plateformes avec une copie de données de production, puisqu' rekey réécrit chaque page.
La chiffrure avec d'autres plugins
| Plugin | Comment fonctionne l'encryption | Gestion des clés |
|---|---|---|
@capgo/capacitor-fast-sql |
Opt-in SQLCipher sur iOS et Android | Vous passez la clé connect() |
@capacitor-community/sqlite |
SQLCipher est toujours intégré, activé dans capacitor.config |
Le plugin stocke une phrase de passe par application avec setEncryptionSecret(), modes secret, encryption (crypter un fichier existant), newsecret |
@capgo/capacitor-data-storage-sqlite |
SQLCipher pour son stockage de valeurs clés | Mot de passe défini capacitor.config, donc elle est embarquée dans l'application |
Le plugin de la communauté encryption mode peut chiffrer une base de données existante sur place, ce qui est pratique si vous êtes déjà dessus. Avec un mot de passe dans capacitor.config (data-storage-sqlite), considérez le chiffrement comme une obscurcissement : le secret est dans le bundle. Pour des valeurs vraiment sensibles, préférez Fast SQL avec une clé provenant d'un stockage sécurisé. Voir key-value storage options dans Capacitor pour ce compromis.
Notes de conformité
- Compliances d'application Lorsque vous utilisez SQLCipher, votre application contient une encryption. App Store Connect demande à ce sujet pour chaque build (ou vous le définissez)
ITSAppUsesNonExemptEncryptioninInfo.plist). Encryption of the user’s own data on the device commonly falls under exemptions, but the answer depends on your app and the countries you distribute to. Check Apple’s guidance and get advice if you are unsure. Our Résumé de l'encodage de l'application app encryption overview - Licence SQLCipher. L'édition communautaire utilise une licence BSD de style qui nécessite sa mention de copyright dans votre application. Ajoutez-l’à votre écran des licences open source.
- Sécurité des données Google Play. Vous pouvez déclarer les données comme chiffrées en place. Assurez-vous que c'est vrai pour chaque copie, y compris les caches et les journaux.
Troubleshooting
file is not a database Sur la première requête. Clé incorrecte, ou le fichier est en clair et vous l'avez ouvert avec encrypted: trueVérifiez que la clé dans le stockage sécurisé est celle utilisée lors de la création, puis utilisez la migration ci-dessus pour les fichiers texte.
Encryption is not available in this build Sur iOS. Le fichier Podfile utilise toujours la sous-espèce par défaut. Ré-ajoutez la CapgoCapacitorFastSql/SQLCipher ligne après cap sync et exécutez pod install.
SQLCipher native library not found Sur Android. La sqlcipher-android La dépendance manque dans le module d'application, ou vous l'avez ajoutée au plugin ou à la racine build.gradle au lieu de cela.
L'ouverture de la base de données est lente. Dérivation de clé s'exécute à chaque ouverture. Ouvrez une fois au démarrage et réutilisez la connexion au lieu de vous connecter par requête.
La base de données s'ouvre sur un appareil mais pas après restauration. La clé n'a pas voyagé avec le sauvegarde. Gérez-l’avec le flux de réinitialisation et réynchronisez-vous à partir du serveur.
Résumé
- Ajoutez SQLCipher :
sqlcipher-androiddans le module d'application Android, leSQLCiphersous-édition sur iOS. - Générez une clé aléatoire de 256 bits lors du premier lancement.
- Stockez-l’avec
@capgo/capacitor-native-biometric(setData, facultativement avecaccessControl). - Ouvrez-l’avec
encrypted: true, puis lisez-l’une fois pour vérifier la clé. - Plan for key loss: reset and resync.
- Migrer les bases de données existantes une fois, et tourner les clés avec
PRAGMA rekeysi votre politique le prévoit.
Native changes like adding SQLCipher need a new store build. Capgo Éditeur peut produire des binaires iOS et Android signés sans Mac local, et le Docs SQL rapide liste toutes les options de connexion.