Langsung ke konten utama

Cara Mengenkripsi Database SQLite di Capacitor

Enkripsi database SQLite di Capacitor dengan SQLCipher: setup di iOS dan Android, simpan key dengan aman, migrasi data yang sudah ada, dan rotasi key.

Logo Capgo untuk panduan mengenkripsi database SQLite di Capacitor dengan SQLCipher

Untuk mengenkripsi database SQLite di Capacitor, gunakan plugin berbasis SQLCipher, buka database dengan encryption key, dan simpan key tersebut di iOS Keychain atau Android Keystore, bukan di code. Panduan ini akan membahasnya @capgo/capacitor-fast-sql di Capacitor 8: setup build untuk kedua platform, pembuatan dan penyimpanan key, enkripsi database yang sudah berisi data user, rotasi key, serta kasus kegagalan yang harus ditangani.

Apa yang dilindungi SQLCipher, dan apa yang tidak

SQLCipher adalah ekstensi open source dari SQLite yang mengenkripsi setiap halaman file database dengan AES-256. Tanpa kunci, file tersebut hanyalah byte acak. Ini juga mencakup file WAL dan journal.

Ini melindungi dari:

  • seseorang yang menyalin file aplikasi dari perangkat yang sudah di-jailbreak atau di-root, backup yang tidak terenkripsi, atau ponsel hilang dengan passcode yang lemah,
  • alat forensik yang membaca container aplikasi,
  • aplikasi lain yang membaca file melalui direktori bersama yang salah konfigurasi.

Ini tidak melindungi dari:

  • code yang berjalan di dalam aplikasi Anda (dependency berbahaya, bug XSS di WebView) yang dapat memanggil data layer Anda,
  • penyerang yang dapat mengekstrak kunci, itulah sebabnya penyimpanan kunci lebih penting daripada cipher-nya sendiri,
  • data yang juga Anda simpan dalam bentuk plaintext di tempat lain: logs, localStorage, cache, event analytics.

iOS dan Android sudah mengenkripsi penyimpanan aplikasi saat perangkat terkunci. SQLCipher menambahkan lapisan yang tidak bergantung pada passcode perangkat dan tetap bertahan melalui backup. Ini sangat penting untuk aplikasi kesehatan, finansial, dan sektor teregulasi lainnya. Untuk gambaran lebih luas tentang ancaman penyimpanan, pertimbangkan juga backup, logs, dan cache plaintext di luar SQLite.

Prerequisites

Fast SQL terpasang dan dikonfigurasi seperti di Cara menggunakan SQLite di aplikasi Capacitor, termasuk pengaturan local networking iOS dan cleartext localhost Android. Enkripsi bersifat opt-in, jadi instalasi dasar tidak menyertakan SQLCipher.

Langkah 1: tambahkan SQLCipher ke build native

Android

Tambahkan dependensi ke app-level android/app/build.gradle:

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

README plugin mengunci versi 4.13.0. Rilis 4.x yang lebih baru tetap berfungsi selama major version tetap 4. Tanpa dependensi ini, encrypted: true gagal dengan error yang jelas (“SQLCipher native library not found”) alih-alih crash.

SQLCipher menyertakan file native .so untuk setiap ABI. Harapakan APK bertambah beberapa megabyte. App Bundles hanya mengirim ABI yang dibutuhkan perangkat.

iOS

Ganti plugin ke subspec SQLCipher-nya di ios/App/Podfile, setelah bunx cap sync telah menulis entri default:

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

Lalu:

cd ios/App && pod install

bunx cap sync meregenerasi bagian Capacitor pada Podfile, jadi periksa baris ini setiap kali sync. Tanpa subspec, plugin akan di-build dengan SQLite sistem, dan saat dibuka dengan encrypted: true mengembalikan Encryption is not available in this build.

Web

Tidak ada SQLCipher di web. Opsi encrypted opsi ini hanya untuk iOS dan Android. Jika aplikasi Anda juga dirilis sebagai PWA, buat cabang berdasarkan Capacitor.isNativePlatform() dan jangan simpan tabel sensitif secara persisten di build browser.

Langkah 2: buat key

Gunakan Web Crypto API, yang tersedia di WebView iOS dan 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('');
}

Ini menghasilkan string hex 64 karakter. SQLCipher memperlakukan string tersebut sebagai passphrase dan menurunkan key sebenarnya dengan PBKDF2-HMAC-SHA512 (default 256.000 iterasi di SQLCipher 4). Derivasi ini berjalan sekali setiap kali database dibuka dan memakan sebagian kecil detik yang cukup terasa di ponsel lama, itu normal.

Jangan pakai password pengguna langsung sebagai key, kecuali Anda memang ingin database terkunci pada password itu. Pengguna lupa password, mengubahnya di perangkat lain, dan memilih yang lemah. Desain yang umum adalah device key acak plus secure storage. Jika butuh akses yang dijaga pengguna, batasi akses ke key-nya saja (dengan biometrik).

Langkah 3: simpan key di Keychain atau Keystore

@capgo/capacitor-native-biometric menyimpan string arbitrer yang terenkripsi saat disimpan menggunakan iOS Keychain dan Android Keystore. setData dan getData (plugin 8.6.0 ke atas) tidak memunculkan prompt ke pengguna. Tambahkan accessControl untuk mewajibkan Face ID, Touch ID, atau sidik jari sebelum key bisa dibaca.

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

Opsional: wajibkan biometrik untuk membuka 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 tetap valid meskipun sidik jari atau wajah baru didaftarkan. BIOMETRY_CURRENT_SET membatalkan key saat pendaftaran berubah; ini lebih ketat, tetapi database lokal jadi tidak terbaca ketika pengguna menambahkan sidik jari. Pilih opsi ini hanya jika data bisa di-resync dari server Anda. Selengkapnya soal prompt dan fallback di autentikasi biometrik di aplikasi Capacitor.

Tempat yang tidak boleh menyimpan key

  • Di JavaScript Anda, .env file, atau capacitor.config.ts. Semuanya disertakan dalam app bundle dan siapa pun bisa unzip file IPA atau APK.
  • Di @capacitor/preferences atau localStorage. Semuanya berupa plaintext.
  • Diambil dari API Anda setiap kali aplikasi dijalankan tanpa salinan lokal. Aplikasi tidak akan bisa dibuka saat offline, sehingga database lokal jadi tidak berguna. Mengambilnya sekali setelah login dan menyimpannya di Keychain atau Keystore tidak masalah.

Langkah 4: buka database terenkripsi

// 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 sangat penting. SQLCipher tidak memvalidasi key saat dibuka. Pembacaan pertama yang sebenarnya akan gagal dengan file is not a database jika key salah, maka segera lakukan pembacaan ringan dan tangani error di satu tempat saja, bukan di tengah layar.

Untuk mengecek apakah enkripsi sudah aktif, jalankan PRAGMA cipher_version pada koneksi. Ini mengembalikan versi SQLCipher, dan hasil kosong pada SQLite biasa.

Langkah 5: tangani key yang hilang atau salah

Key bisa hilang dalam penggunaan normal:

  • Pengguna memulihkan backup di ponsel baru. File database kembali, tetapi item Keychain yang dibuat sebagai device-only dan key Android Keystore tidak ikut pindah ke hardware baru.
  • Android Auto Backup memulihkan file aplikasi, tetapi key Keystore yang digunakan untuk mengenkripsi secret yang disimpan tidak dipulihkan.
  • Pengguna mengubah pendaftaran biometrik dan Anda menggunakan BIOMETRY_CURRENT_SET.

Proses recovery selalu sama: data hilang secara lokal, jadi hapus file dan mulai ulang, lalu sinkronkan ulang dari backend Anda.

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

Panggil saat verifyKey gagal dengan file is not a database, reset dbPromise, lalu buka lagi. Kunci baru akan dibuat dan database kosong akan dibuat.

Dua pengaturan terkait yang perlu ditinjau:

  • Di Android, pertimbangkan untuk mengecualikan database dari Auto Backup di data_extraction_rules.xml jika tidak dapat didekripsi setelah restore. Ini menghemat reset yang membingungkan bagi pengguna Anda.
  • Di iOS, file di Documents masuk ke backup iCloud. Terenkripsi, jadi hanya bisa dibaca dengan kunci, tetapi mengecualikannya membuat backup lebih kecil jika data bisa disinkronkan ulang.

Enkripsi database plaintext yang sudah ada

Jika aplikasi Anda sudah dirilis dengan database tidak terenkripsi, Anda tidak bisa mengaktifkan encrypted: true pada file yang sama. SQLCipher akan mencoba mendekripsi halaman plaintext dan gagal. Salin data ke database terenkripsi baru sekali saja:

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

Jalankan sekali, dilindungi oleh flag yang disimpan setelah sukses, dan tampilkan status progres karena membaca setiap baris. Catatan:

  • Tabel dibuat dan diisi terlebih dahulu, lalu indeks, trigger, dan view. Membangun indeks sekali di akhir lebih cepat, dan trigger tidak aktif saat baris yang sudah ada disalin.
  • sqlite_sequence dilewati. SQLite memperbaruinya secara otomatis saat baris dengan id eksplisit disisipkan, jadi AUTOINCREMENT melanjutkan dari id tertinggi.
  • Tabel virtual (FTS) membuat tabel bayangan internal. Buat ulang tabel FTS dari CREATE VIRTUAL TABLE statement dan bangun ulang index alih-alih menyalin shadow tables.
  • Untuk tabel yang sangat besar, gunakan paging dengan WHERE rowid > ? ORDER BY rowid LIMIT 5000 alih-alih satu SELECT *.
  • Menghapus file plaintext adalah inti dari latihan ini. Flash storage yang sudah dihapus bisa masih menyimpan blok lama, tetapi file tersebut tidak lagi dapat diakses melalui file system.

Rotasi kunci

SQLCipher mengenkripsi ulang seluruh database dengan PRAGMA rekey. Dengan Fast SQL, Anda menjalankannya sebagai statement pada koneksi terenkripsi yang terbuka:

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

Urutan itu penting. Jika aplikasi mati di antara kedua baris tersebut, database memakai key baru sementara Keychain masih menyimpan key lama. Untuk menghindarinya, tulis key baru dengan nama kedua dulu, lakukan rekey, lalu promosikan dan hapus entri lama, dan saat startup coba pending key jika key utama gagal. PRAGMA tidak menerima bound parameters, itulah sebabnya key di-inline. Hanya inline nilai yang Anda buat sendiri, jangan pernah input dari user. Uji rotasi di kedua platform dengan salinan data seukuran produksi, karena rekey menulis ulang setiap page.

Enkripsi dengan plugin lain

Plugin Cara kerja enkripsi Penanganan key
@capgo/capacitor-fast-sql Opt-in SQLCipher di iOS dan Android Anda meneruskan key di connect()
@capacitor-community/sqlite SQLCipher selalu di-bundle, diaktifkan di capacitor.config Plugin menyimpan satu passphrase per app dengan setEncryptionSecret(), mode secret, encryption (enkripsi file yang sudah ada), newsecret
@capgo/capacitor-data-storage-sqlite SQLCipher untuk key-value store-nya Passphrase diatur di capacitor.config, jadi ikut terkirim di app

milik plugin komunitas encryption mode dapat mengenkripsi database yang sudah ada di tempat, yang praktis jika Anda sudah memakainya. Dengan passphrase di capacitor.config anggap enkripsi sebagai obfuscation: secret-nya ada di dalam bundle. Untuk nilai yang benar-benar sensitif, utamakan Fast SQL dengan key dari secure storage. Lihat opsi key-value storage di Capacitor untuk trade-off itu.

Catatan kepatuhan

  • Kepatuhan ekspor App Store. Menyertakan SQLCipher berarti aplikasi Anda berisi enkripsi. App Store Connect menanyakannya untuk setiap build (atau Anda mengatur ITSAppUsesNonExemptEncryption di Info.plist). Enkripsi data pengguna di perangkat biasanya termasuk pengecualian, tetapi jawabannya bergantung pada aplikasi Anda dan negara tujuan distribusi Anda. Cek panduan Apple dan minta saran jika ragu. ringkasan enkripsi aplikasi kami mencakup gambaran yang lebih luas.
  • Lisensi SQLCipher. Edisi komunitas menggunakan lisensi bergaya BSD yang mewajibkan pemberitahuan hak cipta di aplikasi Anda. Tambahkan ke layar lisensi open source Anda.
  • Keamanan data Google Play. Anda dapat mendeklarasikan data sebagai terenkripsi saat diistirahatkan. Pastikan hal ini berlaku untuk setiap salinan, termasuk cache dan logs.

Pemecahan Masalah

file is not a database pada query pertama. Kunci salah, atau file berupa plaintext dan Anda membukanya dengan encrypted: true. Periksa bahwa kunci di secure storage adalah yang digunakan saat pembuatan, lalu gunakan migrasi di atas untuk file plaintext.

Encryption is not available in this build di iOS. Podfile masih menggunakan subspec default. Tambahkan kembali CapgoCapacitorFastSql/SQLCipher baris setelah cap sync lalu jalankan pod install.

SQLCipher native library not found di Android. Panduan sqlcipher-android dependency-nya hilang dari modul app, atau Anda menambahkannya ke plugin atau root build.gradle instead.

Membuka database terasa lambat. Key derivation berjalan setiap kali dibuka. Buka sekali saat startup dan pakai ulang koneksinya, bukan membuat koneksi baru tiap query.

Database bisa dibuka di satu perangkat, tapi tidak setelah restore. Kunci tidak ikut tercadangkan. Tangani dengan alur reset, lalu sinkronisasi ulang dari server.

Ringkasan

  1. Tambahkan SQLCipher: sqlcipher-android di modul app Android, dan SQLCipher subspec di iOS.
  2. Generate key 256-bit secara acak saat pertama kali dijalankan.
  3. Simpan dengan @capgo/capacitor-native-biometric (setData, opsional dengan accessControl).
  4. Buka dengan encrypted: true, lalu baca sekali untuk memverifikasi key.
  5. Plan for key loss: reset and resync.
  6. Migrasikan database plaintext yang ada sekali, dan rotasi key dengan PRAGMA rekey jika kebijakan Anda memerlukannya.

Perubahan native seperti menambahkan SQLCipher butuh build store baru. Build Capgo dapat menghasilkan binary iOS dan Android yang ditandatangani tanpa Mac lokal, dan Dokumentasi Fast SQL mencantumkan semua opsi koneksi.

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.