Crittografia Locale SQLite con SQLCipher su iOS & Android
Come blindare i dati sensibili a riposo su smartphone: crittografia a livello di pagina AES-256 con SQLCipher, ancoraggio della chiave nel Secure Enclave / Android Keystore e prevenzione dell'estrazione forense.
1. Il Falso Senso di Sicurezza di SQLite
Molte app archiviano dati biometrici o finanziari in un normale file database.sqlite confidando nella sandbox del sistema operativo (iOS Data Protection o Android app isolation).
Questo approccio fallisce contro:
- Dispositivi con Jailbreak o permessi di Root.
- Backup locali non cifrati (es. backup iTunes/Finder via USB o strumenti ADB).
- Analisi forense su chip di memoria fisica (chip-off analysis).
SQLCipher risolve il problema alla radice: ogni singola pagina da 4096 byte del database viene cifrata con standard AES-256 (CBC o GCM) con HMAC per pagina per prevenire manomissioni.
2. Generazione e Custodia Hardware della Chiave
Non salvare mai la passphrase come stringa nel codice sorgente! Genera una chiave simmetrica casuale da 256 bit e salvala nel chip di sicurezza hardware del dispositivo.
Implementazione in Swift (iOS Keychain & Secure Enclave):
import Foundation
import Security
public final class KeychainKeyManager {
private static let keyTag = "dev.alexmaina.rosettaedge.vaultkey"
public static func getOrCreateMasterKey() throws -> Data {
// 1. Tenta il recupero da Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keyTag,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess, let keyData = item as? Data {
return keyData
}
// 2. Se non esiste, genera 32 byte crittograficamente sicuri (256 bit)
var keyData = Data(count: 32)
let result = keyData.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
}
guard result == errSecSuccess else { throw KeyStoreError.generationFailed }
// 3. Salva nel Keychain con protezione al primo sblocco del device
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keyTag,
kSecValueData as String: keyData,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
SecItemAdd(addQuery as CFDictionary, nil)
return keyData
}
}
3. Apertura del Database Criptato con SQLCipher (Swift)
Subito dopo aver aperto la connessione SQLite, esegui il comando PRAGMA key prima di qualsiasi altra operazione:
import SQLCipher
func openEncryptedVault(path: String, keyData: Data) throws -> OpaquePointer? {
var db: OpaquePointer?
if sqlite3_open(path, &db) == SQLITE_OK {
// Applica la chiave crittografica grezza (raw hex)
let hexKey = keyData.map { String(format: "%02hhx", $0) }.joined()
let pragmaQuery = "PRAGMA key = \"x'\(hexKey)'\";"
if sqlite3_exec(db, pragmaQuery, nil, nil, nil) == SQLITE_OK {
// Verifica che la chiave sia valida eseguendo una lettura test
if sqlite3_exec(db, "SELECT count(*) FROM sqlite_master;", nil, nil, nil) == SQLITE_OK {
print("Vault aperto e decifrato con successo!")
return db
}
}
}
throw VaultError.invalidKeyOrCorrupted
}
4. Integrazione con Android Room (Kotlin)
Su Android, SQLCipher si integra nativamente con Room Database tramite SupportFactory:
import android.content.Context
import androidx.room.Room
import net.sqlcipher.database.SupportFactory
import net.sqlcipher.database.SQLiteDatabase
fun buildEncryptedRoomDatabase(context: Context, passphraseBytes: ByteArray): AppDatabase {
// 1. Inizializza le librerie binarie C native di SQLCipher
SQLiteDatabase.loadLibs(context)
// 2. Crea la factory crittografica
val factory = SupportFactory(passphraseBytes)
// 3. Costruisci il database Room cifrato
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"rosetta_vault.db"
)
.openHelperFactory(factory)
.build()
}
❓ Domande Frequenti (FAQ Tecniche)
Qual è l'impatto di performance di SQLCipher rispetto a SQLite?
Sui moderni processori con accelerazione crittografica hardware integrata (AES-NI / NEON), l'overhead è compreso tra il 3% e l'8%. È impercettibile per l'utente anche con query complesse su decine di migliaia di record.
Cosa succede se l'utente perde la chiave di cifratura?
I dati rimangono matematicamente irrecuperabili. Per questo motivo la chiave deve essere gestita in modo sicuro tramite il Keychain di sistema protetto da biometria (Face ID / Impronta).
SQLCipher cifra anche gli indici e i file temporanei?
Sì, ogni pagina di dati, indice, albero B-Tree e file Write-Ahead Log (WAL) viene cifrata integralmente prima di toccare la memoria flash del telefono.