Para cifrar una base de datos SQLite en Capacitor, utilice un plugin construido con SQLCipher, abra la base de datos con una clave de cifrado, y mantenga esa clave en el Keychain de iOS o en el Keystore de Android en lugar de en su code. Este guía lo hace con @capgo/capacitor-fast-sql en Capacitor 8: configuración de instalación para ambas plataformas, generación y almacenamiento de claves, cifrado de una base de datos que ya tiene datos de usuario, rotación de claves, y los casos de falla que tiene que manejar.
¿Qué protege SQLCipher, y qué no
SQLCipher es una extensión de código abierto de SQLite que cifra cada página del archivo de base de datos con AES-256. Sin la clave, el archivo es bytes aleatorios. También cubre los archivos WAL y de registro.
Lo protege contra:
- Alguien copiando los archivos de la aplicación desde un dispositivo jailbreakado o rootado, un respaldo no cifrado o un teléfono perdido con una contraseña débil.
- herramientas forenses que leen contenedores de aplicaciones
- otras aplicaciones que lean el archivo a través de un directorio compartido configurado incorrectamente
No lo protege contra:
- code ejecutándose dentro de tu aplicación (una dependencia maliciosa, un bug XSS en el WebView) que puede llamar a tu capa de datos
- un atacante que pueda extraer la clave, por lo que la almacenamiento de claves es más importante que el cifrado
- datos que también guardas en texto plano en otros lugares: registros.
localStorage, cachés, eventos de análisis.
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 almacenamiento de base de datos seguro.
Requisitos previos
Fast SQL instalado y configurado como en Cómo usar SQLite en una aplicación Capacitor, incluyendo las configuraciones de red local iOS y Android localhost sin texto claro. La cifrado es de opción, por lo que la instalación base no incluye SQLCipher.
Step 1: agregar SQLCipher a las compilaciones nativas
Android
agrega la dependencia a la app-level android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}
El plugin README establece la versión 4.13.0. Las versiones 4.x más recientes funcionan siempre que la versión mayor sea 4. Sin esta dependencia, encrypted: true falla con un error claro (“no se encuentra la biblioteca nativa SQLCipher”) en lugar de bloquear.
SQLCipher envía nativamente .so Archivos para cada ABI. Espera que el APK aumente en unos megabytes. Los paquetes de aplicaciones solo entregan el ABI que cada dispositivo necesita.
IOS
Cambie el plugin a su subspec SQLCipher en ios/App/Podfile, después de bunx cap sync ha escrito la entrada predeterminada:
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
Luego:
cd ios/App && pod install
bunx cap sync regenera la sección Capacitor del archivo Podfile, así que revisa esta línea después de cada sincronización. Sin la subspec, el plugin compila contra el SQLite del sistema, y abrir con encrypted: true devuelve Encryption is not available in this build.
Web
No hay SQLCipher en la web. La opción de Fast SQL encrypted es solo para iOS y Android. Si tu aplicación también se envía como una PWA, haz una rama en Capacitor.isNativePlatform() y no persistas tablas sensibles en la compilación del navegador.
Paso 2: generar una clave
Utilice el Web Crypto API, que está disponible en los navegadores iOS y 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('');
}
Esto da una cadena de 64 caracteres en hexadecimal. SQLCipher trata la cadena como una contraseña y deriva la clave real con PBKDF2-HMAC-SHA512 (256.000 iteraciones por defecto en SQLCipher 4). Esa derivación se ejecuta una vez por abrir y cuesta una fracción notoria de un segundo en teléfonos más antiguos, lo cual es normal.
No utilice una contraseña de usuario directamente como la clave a menos que desee que la base de datos esté bloqueada con esa contraseña. Los usuarios olvidan contraseñas, las cambian en otro dispositivo y eligen contraseñas débiles. Una clave aleatoria de dispositivo más almacenamiento seguro es el diseño usual. Si necesita acceso controlado por el usuario, controle el acceso a la clave (con biométricas) en su lugar.
Paso 3: almacene la clave en la Keychain o Keystore
@capgo/capacitor-native-biometric almacena cadenas arbitrarias cifradas en reposo con la Keychain de iOS y el Keystore de Android. setData y getData (plugin 8.6.0 y posterior) no solicite al usuario. Agregue accessControl requiere Face ID, Touch ID o huella dactilar antes de que la clave pueda ser leída.
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 });
}
opcional: requiere biométricas para desbloquear la base de datos
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 sobrevive a la inscripción de un nuevo dedo o cara. BIOMETRY_CURRENT_SET invalida la clave cuando cambian las inscripciones, lo cual es más estricto pero significa que la base de datos local se vuelve inaccesible cuando el usuario agrega un dedo. Solo elija esta opción si los datos pueden ser sincronizados nuevamente desde su servidor. Más sobre solicitudes y respaldos en autenticación biométrica en aplicaciones Capacitor.
¿Dónde no poner la clave
- En tus archivos JavaScript
.envocapacitor.config.tsTodos ellos se envían dentro de la caja de la aplicación y cualquier persona puede descomprimir un IPA o APK - En
@capacitor/preferencesolocalStorageAquellas son texto plano. - 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.
Paso 4: abrir la base de datos cifrada
// 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 No importa los asuntos. SQLCipher no valida la clave en el momento de abrir. La primera lectura real falla con file is not a database Importa. SQLCipher no valida la clave en el momento de abrir. La primera lectura real falla con
To check that encryption is active, run PRAGMA cipher_version en la conexión. Devuelve la versión de SQLCipher, y un resultado vacío en SQLite plano.
Paso 5: manejar una clave perdida o incorrecta
Una clave puede desaparecer en el uso normal:
- El usuario restaura una copia de seguridad en un nuevo teléfono. El archivo de la base de datos vuelve a aparecer, pero los elementos de Keychain creados como dispositivos únicos y las claves de Android Keystore no se mueven a nuevos hardware.
- Android Auto Backup restaura los archivos de la aplicación, pero la clave de Keystore utilizada para cifrar el secreto almacenado no se restauró.
- El usuario cambia la inscripción biométrica y usted utilizó
BIOMETRY_CURRENT_SET.
La recuperación es siempre la misma: los datos están perdidos localmente, así que elimine el archivo y comience de nuevo, luego sincronice desde su 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();
}
Llámalo cuando verifyKey falla con file is not a database, reinicie dbPromise, luego abra de nuevo. Se genera una nueva clave y se crea una base de datos vacía.
Dos ajustes relacionados para revisar:
- On Android, consider excluir la base de datos de la copia automática en
data_extraction_rules.xmlsi no se puede descifrar después de restaurar de todos modos. Ahorra a tus usuarios un confuso reinicio. - On iOS, los archivos en
Documentsvan a respaldos de iCloud. Cifrado, por lo que solo son legibles con la clave, pero excluirlos mantiene los respaldos pequeños si los datos pueden ser sincronizados nuevamente.
Cifrar una base de datos existente en texto plano
Si su aplicación ya se envió con una base de datos no cifrada, no puede cambiar encrypted: true en el mismo archivo. SQLCipher intentaría descifrar páginas de texto plano y fallaría. Copia los datos a una nueva base de datos cifrada una vez:
// 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;
}
Ejecútalo una vez, protegido por una bandera almacenada después de éxito, y muestra un estado de progreso porque lee cada fila. Notas:
- Las tablas se crean y se llenan primero, los índices, los disparadores y las vistas después. Construir índices una vez al final es más rápido, y los disparadores no disparan mientras se copian las filas existentes.
sqlite_sequencese salta. SQLite lo actualiza automáticamente cuando se insertan filas con ids explícitos, por lo queAUTOINCREMENTcontinúa desde el id más alto.- Tablas virtuales (FTS) crean tablas de sombra internas. Recrea tablas FTS a partir de sus
CREATE VIRTUAL TABLEdeclaración y reconstruir el índice en lugar de copiar las tablas de sombra. - Para tablas muy grandes, recorre con
WHERE rowid > ? ORDER BY rowid LIMIT 5000en lugar de una solaSELECT *. - Eliminar el archivo de texto plano es el punto de la ejercicio. El almacenamiento flash eliminado todavía puede contener bloques antiguos, pero el archivo ya no es accesible a través del sistema de archivos.
Rotar la clave
SQLCipher re-encRIPTA la base de datos completa con PRAGMA rekeyCon Fast SQL se ejecuta como una declaración en una conexión cifrada abierta:
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 });
}
El orden importa. Si la aplicación se cierra entre las dos líneas, la base de datos utiliza la nueva clave mientras la Keychain todavía tiene la antigua. Para evitar eso, escriba la nueva clave con un nombre secundario primero, reclave, luego promueva y elimine la entrada antigua, y al iniciar intenta la clave pendiente si la principal falla. PRAGMA no acepta parámetros vinculados, lo que es por qué la clave está inyectada. Solo inyecta valores que generaste, nunca la entrada del usuario. Prueba la rotación en ambas plataformas con una copia de datos de tamaño de producción, ya que rekey reescrive cada página.
La cifrado con otros plugins
| Plugin | Cómo funciona la cifrado | Gestión de claves |
|---|---|---|
@capgo/capacitor-fast-sql |
Opción de SQLCipher en iOS y Android | You pass the key on connect() |
@capacitor-community/sqlite |
SQLCipher siempre incluido, habilitado en capacitor.config |
El plugin almacena una contraseña por aplicación con setEncryptionSecret(), modos secret, encryption (cifra un archivo existente), newsecret |
@capgo/capacitor-data-storage-sqlite |
SQLCipher para su almacén de claves | Contraseña configurada en capacitor.config, por lo que se envía en la aplicación |
El plugin de la comunidad encryption El modo puede cifrar una base de datos existente en su lugar, lo que es conveniente si ya está en ella. Con una contraseña en capacitor.config (almacenamiento-de-datos-sqlite), trate la cifrado como una obstrucción: el secreto está en el paquete. Para valores verdaderamente sensibles, prefiera Fast SQL con una clave desde un almacenamiento seguro. Consulte las opciones de almacenamiento de valores en Capacitor para ese equilibrio.
Comentarios de cumplimiento
- Cumplimiento de exportación de la Tienda de Aplicaciones. Envío de SQLCipher significa que tu aplicación contiene cifrado. App Store Connect pregunta sobre ello para cada compilación (o estableces
ITSAppUsesNonExemptEncryptionenInfo.plist). La cifrado de los datos del usuario en el dispositivo suele estar bajo exenciones, pero la respuesta depende de tu aplicación y los países a los que distribuyes. Verifica la guía de Apple y obtén asesoramiento si no estás seguro. Nuestra Resumen de la cifrado de la aplicación aborda la imagen más amplia. - licencia de SQLCipher. La edición de la comunidad utiliza una licencia de estilo BSD que requiere su nota de derechos de autor en su pantalla de licencias de código abierto.
- Seguridad de datos de Google Play. Puede declarar los datos como cifrados en reposo. Asegúrese de que sea cierto para cada copia, incluidos los cachés y los registros.
Solución de problemas
file is not a database en la primera consulta. La clave es incorrecta, o el archivo es de texto plano y lo abriste con encrypted: trueVerifique que la clave en almacenamiento seguro sea la misma que la utilizada en la creación, luego utilice la migración anterior para archivos de texto plano.
Encryption is not available in this build en iOS. El archivo Podfile todavía utiliza la subspec predeterminada. Vuelve a agregar la CapgoCapacitorFastSql/SQLCipher linea después cap sync y ejecutar pod install.
SQLCipher native library not found y ejecute La sqlcipher-android La dependencia falta en el módulo de la aplicación, o la agregaste al plugin o a la raíz build.gradle en lugar.
Abrir la base de datos es lento. Deriva la clave en cada apertura. Abre una vez al arranque y reutiliza la conexión en lugar de conectarte por consulta.
La base de datos se abre en un dispositivo pero no después de restaurar. La clave no viajó con la copia de seguridad. Maneje esto con el flujo de reinicio y resincronice desde el servidor.
Resumen
- Agregar SQLCipher:
sqlcipher-androiden el módulo de la aplicación de Android, elSQLCiphersubespecífico en iOS. - Generar una clave aleatoria de 256 bits en la primera ejecución.
- Almacénalo con
@capgo/capacitor-native-biometric(setData, opcionalmente conaccessControl). - Abre con
encrypted: true, luego lee una vez para verificar la clave. - Planifica la pérdida de clave: reinicia y sincroniza.
- Migrar bases de datos existentes de texto plano una vez, y rotar claves con
PRAGMA rekeysi tu política lo requiere.
Cambios nativos como agregar SQLCipher requieren una nueva construcción de almacenamiento. Capgo Construye puede producir binarios firmados para iOS y Android sin una Mac local, y el Documentos SQL rápido listar todas las opciones de conexión.