SQLite 데이터베이스를 Capacitor에서 암호화하려면 SQLCipher로 빌드된 플러그인을 사용하고 데이터베이스를 암호화 키로 열고 iOS Keychain 또는 Android Keystore에 그 키를 저장하는 대신 code에 저장하지 않습니다. 이 안내서에서는 @capgo/capacitor-fast-sql Capacitor 8: 두 플랫폼 모두에 대한 빌드 설정, 키 생성 및 저장, 이미 사용자 데이터가 있는 데이터베이스를 암호화하는 방법, 키 회전 및 처리해야 하는 실패 사례를 다룹니다.
SQLCipher가 보호하는 것과 보호하지 않는 것
SQLCipher는 SQLite의 확장 버전으로 데이터베이스 파일의 모든 페이지를 AES-256로 암호화합니다. 키가 없으면 파일은 랜덤 바이트입니다. WAL 및 로그 파일도 포함됩니다.
SQLCipher는 다음과 같은 것을 보호합니다:
- someone이 앱의 파일을 기기에서 복사하거나 앱을 해킹하거나 루트를 풀어도 데이터베이스를 복사할 수 없으며, 백업이 암호화되지 않았거나 패스코드가 약한 기기에 데이터베이스가 손실되면
- forensic 도구가 앱 컨테이너를 읽을 수 없으며
- 다른 앱이 잘못된 공유 폴더를 통해 파일을 읽는 경우.
다음과 같은 공격에 대비하지 않습니다.
- code이 앱 내에서 실행 중인 경우 (악성 의존성, WebView의 XSS 버그) 데이터 레이어를 호출할 수 있습니다.
- 키를 추출할 수 있는 공격자, 키 저장이 암호화보다 더 중요하기 때문입니다.
- 플레인 텍스트로 저장한 데이터: 로그, 캐시, 분석 이벤트.
localStorageiOS와 Android는 기기에 잠금이 걸렸을 때 앱 저장소를 암호화합니다. SQLCipher는 기기 잠금에 의존하지 않고 백업을 살아남는 추가层을 제공합니다. 건강, 금융, 규제에 관련된 모든 경우에 가치가 있습니다. 보다 광범위한 저장소 위협에 대한 보다 자세한 정보는
안전한 데이터베이스 저장 사전 요구 사항.
Fast SQL이
Capacitor 앱에서 SQLite 사용하기 SQLite 데이터베이스를 Capacitor 앱에서 사용하는 방법SQLCipher를 포함하여 iOS의 로컬 네트워킹 및 Android의 localhost 클리어 텍스트 설정도 포함됩니다. SQLCipher는 옵트인 기능이므로 기본 설치에는 포함되지 않습니다.
1단계: SQLCipher를 네이티브 빌드로 추가하세요
안드로이드
의존성을 앱 수준으로 추가하세요 앱 수준 android/app/build.gradle:
dependencies {
implementation 'net.zetetic:sqlcipher-android:4.13.0'
}
플러그인 README에서 4.13.0을 지정합니다. 4.x의 최신 릴리스는 메이저 버전이 4인 경우에만 작동합니다. 이 의존성을 추가하지 않으면 encrypted: true SQLCipher native 라이브러리가 발견되지 않음
SQLCipher는 각 ABI에 네이티브 .so 파일을 제공합니다. APK의 크기는 몇 메가바이트가 증가할 것입니다. 앱 번들에서는 각 기기에 필요한 ABI만 전달합니다.
iOS
플러그인을 SQLCipher subspec로 Switch하세요 ios/App/Podfile, bunx cap sync 이후에
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
그런 다음:
cd ios/App && pod install
bunx cap sync Podfile의 Capacitor 섹션을 다시 생성하기 때문에, 매 싱크 후에 이 줄을 확인하세요. subspec이 없으면 플러그인은 시스템 SQLite에 빌드하고, encrypted: true 반환 Encryption is not available in this build.
웹
웹에 SQLCipher가 없습니다. Fast SQL의 encrypted iOS와 Android만 지원합니다. 앱이 PWA로 배포되면, Capacitor.isNativePlatform() 2단계: 키 생성
웹 크립토 __CAPGO_KEEP_0__을 사용하세요. iOS와 Android의 웹뷰에서 사용할 수 있습니다:
웹 크립토(Web Crypto) API를 사용하세요. iOS와 Android WebViews에서 사용할 수 있습니다.
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('');
}
사용자 패스워드를 직접 키로 사용하지 마세요. 사용자가 패스워드를 잊거나 다른 기기에서 변경하거나 약한 패스워드를 선택할 수 있기 때문입니다. 일반적인 설계는 랜덤한 기기 키와 안전한 저장소를 사용하는 것입니다. 사용자에 의한 접근 제어가 필요하다면, 접근을 키에 대한 접근 제어 (비омет릭스)로 대신하세요.
3단계: 키를 Keychain 또는 Keystore에 저장
context
@capgo/capacitor-native-biometric iOS Keychain과 Android Keystore에서 암호화된 상태로 저장하는 임의의 문자열을 저장합니다. setData 그리고 getData Face ID, Touch ID 또는 지문 인증을 필요로 하기 전에 키를 읽을 수 있도록 하세요. accessControl 선택 사항: 데이터베이스를 잠그기 위해 생체 인증을 필요로하세요.
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 });
}
새로운 지문 또는 얼굴 등록이 발생하면 키가 유효하지 않습니다.
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 새로운 지문이나 얼굴 등록이 발생해도 살아남습니다. BIOMETRY_CURRENT_SET 키를 저장하지 말아야 할 곳 biometric authentication in Capacitor apps.
파일
- 모두 앱 번들에 포함되어 있으며, Anyone이 IPA 또는 APK를 압축 해제할 수 있습니다.
.env생체 인증capacitor.config.ts__CAPGO_KEEP_0__ - In
@capacitor/preferences또는localStorageFetched from your __CAPGO_KEEP_0__ 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. - 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.
Step 4: 암호화된 데이터베이스를 열기
// 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 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. file is not a database To check that encryption is active, run
Step 5: handle a lost or wrong key PRAGMA cipher_version A key can go missing in normal use:
5단계: 잃어버린 키나 잘못된 키를 처리하십시오
or
- Those are plaintext.
- 안드로이드 오토 백업은 앱의 파일을 복원하지만, 데이터베이스를 암호화하기 위해 사용한 Keystore 키는 복원되지 않았습니다.
- 사용자는 생체 인식 등록을 변경하고,
BIOMETRY_CURRENT_SET.
복구는 항상 동일합니다: 데이터는 로컬에서 사라지므로 파일을 삭제하고 다시 시작하고, 백엔드에서 다시 동기화하세요.
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();
}
호출할 때 verifyKey 실패하면 file is not a database, 데이터베이스를 재설정하세요. dbPromise관련된 두 가지 설정을 검토하세요:
안드로이드에서,
- 에서 데이터베이스를 백업에서 제외하는 것을 고려하세요. 복원 후 암호화가 불가능하다면.
data_extraction_rules.xml사용자에게 혼란스러운 리셋을 피하기 위해. - iOS에서,
Documents파일은 iCloud 백업에 가집니다. 암호화되어 있으므로 키만으로 읽을 수 있지만, 백업이 작아지도록 제외하는 것이 좋습니다. 데이터가 다시 동기화될 수 있다면.
SQLite 데이터베이스 암호화
이미 배포된 앱의 평문 데이터베이스가 있다면 암호화된 데이터베이스로 복사해야 합니다. encrypted: true SQLCipher는 암호화된 데이터베이스에서 평문 페이지를 암호화하려고 시도하고 실패합니다. 데이터를 복사한 후에만 암호화된 데이터베이스를 사용하세요.
// 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;
}
성공 후 플래그를 저장하고 데이터를 복사한 후에만 사용하세요. 진행 상태를 표시해야 합니다.
- 표는 먼저 생성되고 채워집니다. 인덱스, 트리거 및 뷰는 나중에 생성됩니다. 인덱스를 마지막에 생성하는 것이 더 빠르며, 트리거는 기존 행을 복사하는 동안 실행되지 않습니다.
sqlite_sequenceSQLite는 explicit id를 가진 행을 삽입할 때 자동으로 인덱스를 업데이트하므로, id가 가장 높은 행부터 복사합니다.AUTOINCREMENTFTS(풀 텍스트 검색) 테이블은 내부의 그림자 테이블을 생성합니다. 그림자 테이블 대신 FTS 테이블의 statement를 다시 생성하고 인덱스를 재건하세요.- 대용량 테이블의 경우,
CREATE VIRTUAL TABLE대용량 테이블의 경우, - For very large tables, page through with
WHERE rowid > ? ORDER BY rowid LIMIT 5000플래시 스토리지에서 삭제된 데이터는 여전히 이전 블록을 보유하고 있지만, 파일 시스템을 통해 접근할 수 없습니다.SELECT *. - Deleting the plaintext file is the point of the exercise. Deleted flash storage can still hold old blocks, but the file is no longer reachable through the file system.
키를 회전합니다.
SQLCipher는 전체 데이터베이스를 다시 암호화합니다. PRAGMA rekeyFast SQL은 열린 암호화된 연결에서 statement를 실행합니다.
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 });
}
순서는 중요합니다. 앱이 두 줄 사이에 종료되면 데이터베이스는 새로운 키를 사용하며 Keychain에는 이전 키가 남아 있습니다. 이를 피하기 위해, 새로운 키를 두 번째 이름으로 작성하고, 키를 재설정하고, 이전 키를 삭제하고, 시작 시 메인 키가 실패하면 대기 중인 키를 시도하세요. PRAGMA inline된 키는 bound parameter를 받지 않습니다. 따라서 inline된 키는 사용자 입력이 아닌 생성된 키만 사용하세요. 테스트를両 플랫폼에서 프로덕션 크기의 데이터 복사본으로 수행하세요. rekey 페이지를 모두 다시 작성합니다.
다른 플러그인과 함께 암호화
| 플러그인 | 암호화 방법 | 키 관리 |
|---|---|---|
@capgo/capacitor-fast-sql |
iOS와 Android에서 옵트인 SQLCipher | 키를 전달합니다. connect() |
@capacitor-community/sqlite |
SQLCipher는 항상 포함되어 있으며 capacitor.config |
이 플러그인은 하나의 패스워드를 앱당 저장합니다. setEncryptionSecret(), 모드 secret, encryption (이미 존재하는 파일을 암호화하는 경우), newsecret |
@capgo/capacitor-data-storage-sqlite |
SQLCipher는 키-값 저장소에 사용됩니다. | 패스워드가 설정된 경우 capacitor.config, 따라서 앱에 포함되어 shipped됩니다. |
커뮤니티 플러그인의 encryption 모드는 이미 존재하는 데이터베이스를 원본 위치에서 암호화할 수 있습니다. 이는 이미 사용 중인 경우 편리합니다. 패스워드가 있는 경우 capacitor.config (data-storage-sqlite), 암호화를 가리기만 하다고 간주합니다: 비밀은 패키지에 있습니다. truly sensitive한 값은 Fast SQL을 사용하고 secure storage에서 키를 사용하는 것을 선호합니다. See Capacitor 에서 그 트레이드오프를 참조하세요.
Compliance notes
- 애플 스토어 출하 준수 Shipping SQLCipher는 앱에 암호화가 포함됩니다. 애플 스토어 연결은 각 빌드에 대해 질문합니다 (또는 사용자가)
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 사용자의 장치에 암호화된 데이터는 일반적으로 면제되지만, 앱과 배포 국가에 따라 답변이 달라집니다. Apple의 지침을 확인하고 자신이 불확실한 경우 조언을 구하세요. 우리의 앱 암호화 개요 - covers broader picture. SQLCipher 라이선스.
- The community edition uses a BSD-style license that requires its copyright notice in your app. Add it to your open source licenses screen. 구글 플레이 데이터 안전.
Troubleshooting
file is not a database 첫 번째 쿼리에서. 잘못된 키거나 파일이 평문이고 encrypted: true을 열었을 때.
Encryption is not available in this build iOS에서 사용합니다. iOS에서. CapgoCapacitorFastSql/SQLCipher 라인 후에 cap sync 라인을 추가하고 pod install.
SQLCipher native library not found 를 실행하세요. Android에서. sqlcipher-android 앱 모듈에 build.gradle 존재하지 않거나 플러그인이나
에 추가했습니다. 개별 열기마다 키 도출이 실행됩니다. 시작 시 한번 열고, 쿼리당 연결을 맺지 않고 재사용하는 것이 좋습니다.
한 기기에서 데이터베이스가 열리지만 복원 후에는 열리지 않습니다. 키는 백업과 함께 이동하지 않았습니다. 서버에서 다시 동기화하고 리셋 플로우를 처리하세요.
요약
- SQLCipher를 추가하세요:
sqlcipher-androidAndroid 앱 모듈에서SQLCipheriOS의 - 첫 번째 런칭 시 256비트 난수 키를 생성하세요.
- 키를 저장하세요:
@capgo/capacitor-native-biometric(setData, 옵션적으로accessControl). - 열기:
encrypted: true, 키를 확인하기 위해 한 번 읽습니다. - 키 손실 대비 계획: 초기화 및 재同步.
- 기존 평문 데이터베이스를 한 번만 마이그레이션하고 키를 회전하세요.
PRAGMA rekey정책이 요구하는 경우.
SQLCipher와 같은 네이티브 변경은 새로운 저장소 빌드를 필요로 합니다. Capgo 빌드 Capgo 제품/브랜드 및 개발자 용어를 정확하게 유지하세요. Capgo 솔루션 마케팅 페이지. Capgo Ionic Enterprise 플러그인.