Secure your LibXMTP database with SQLCipher encryption
LibXMTP uses SQLite for local storage and supports optional encryption via SQLCipher. This guide covers enabling encryption, managing keys, and best practices for securing user data.
Important: Database encryption is strongly recommended for production applications. However, key derivation, storage, and secure management are the responsibility of the application developer.
use argon2::{Argon2, PasswordHasher};use argon2::password_hash::{SaltString, rand_core::OsRng};fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32], Error> { let argon2 = Argon2::default(); let salt_string = SaltString::encode_b64(salt).map_err(|_| Error::InvalidSalt)?; let hash = argon2.hash_password(password.as_bytes(), &salt_string) .map_err(|_| Error::KeyDerivation)?; let key_bytes = hash.hash.ok_or(Error::KeyDerivation)?; let mut key = [0u8; 32]; key.copy_from_slice(&key_bytes.as_bytes()[..32]); Ok(key)}
Always use a proper Key Derivation Function (KDF) like Argon2, PBKDF2, or scrypt. Never use plain password hashing algorithms like SHA-256 for key derivation.
// SQLCipher is automatically enabled on native platforms// WASM does not support encryption#[cfg(not(target_arch = "wasm32"))]fn is_sqlcipher_available() -> bool { // SQLCipher is built-in for native true}#[cfg(target_arch = "wasm32")]fn is_sqlcipher_available() -> bool { // Encryption not supported in WASM false}
SQLCipher uses secure defaults, but you can customize if needed:
use xmtp_db::encrypted_store::database::native::sqlcipher_connection::SqlCipherConnection;// Default settings (recommended):// - PRAGMA cipher_page_size = 4096// - PRAGMA kdf_iter = 256000// - PRAGMA cipher_hmac_algorithm = HMAC_SHA512// - PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512// These are applied automatically when you provide an encryption key
The default SQLCipher settings provide strong security. Only modify cipher settings if you have specific security requirements and understand the implications.
use xmtp_db::EncryptedMessageStore;fn rotate_encryption_key( old_db_path: &str, old_key: Vec<u8>, new_key: Vec<u8>,) -> Result<(), Error> { // Open old database let old_db = EncryptedMessageStore::new( StorageOption::Persistent(old_db_path), Some(old_key), )?; // Create new database with new key let new_db_path = format!("{}.new", old_db_path); let new_db = EncryptedMessageStore::new( StorageOption::Persistent(&new_db_path), Some(new_key), )?; // Export and import data // Note: This requires implementing data migration // LibXMTP doesn't provide built-in migration // After successful migration: std::fs::rename(&new_db_path, old_db_path)?; Ok(())}
// WASM: Encryption key is ignored#[cfg(target_arch = "wasm32")]let db = EncryptedMessageStore::new( StorageOption::Persistent(path), encryption_key, // Will log warning and ignore)?;
In WASM environments, the database is stored in IndexedDB without encryption. Consider this when handling sensitive data in web applications.
// Error: "file is not a database"// Cause: Wrong encryption key or corrupted database// Try with correct keylet db = EncryptedMessageStore::new( StorageOption::Persistent(path), Some(correct_key),)?;// If still failing, database may be corrupted// You'll need to recreate it (data will be lost)
// Encryption overhead is typically < 5%// No special optimization neededlet db = EncryptedMessageStore::new( StorageOption::Persistent(path), Some(key),)?; // Performance is comparable to unencrypted