// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT) use crate::connection_strategy::ConnectionStrategy; use crate::SqLiteDataStorageError; use rusqlite::Connection; use hex::ToHex; use zeroize::{ZeroizeOnDrop, Zeroizing}; #[allow(dead_code)] #[derive(Debug, ZeroizeOnDrop, Clone)] /// Representation of a SQLCipher key used to unlock a database. pub enum SqlCipherKey { /// Passphrase based key. Passphrase(String), /// Raw key material without a salt value. RawKey([u8; 32]), /// Raw key material with a salt value. RawKeyWithSalt([u8; 48]), } fn blob_string_repr(val: &[u8]) -> String { format!("x'{}'", val.encode_hex_upper::()) } impl SqlCipherKey { fn to_key_pragma_value(&self) -> Zeroizing { Zeroizing::new(match self { SqlCipherKey::Passphrase(pass) => pass.clone(), SqlCipherKey::RawKey(key) => blob_string_repr(key.as_slice()), SqlCipherKey::RawKeyWithSalt(key) => blob_string_repr(key.as_slice()), }) } } #[derive(Debug, Clone)] /// SQLCipher connection config. pub struct SqlCipherConfig { key: SqlCipherKey, plaintext_header_size: u8, } impl SqlCipherConfig { /// Create a new config with a specific key. pub fn new(key: SqlCipherKey) -> SqlCipherConfig { SqlCipherConfig { key, plaintext_header_size: 0, } } /// Adjust the plaintext header size. pub fn with_plaintext_header(self, size: u8) -> SqlCipherConfig { SqlCipherConfig { plaintext_header_size: size, ..self } } } /// Encrypted database connection with SQLCipher. pub struct CipheredConnectionStrategy where I: ConnectionStrategy, { inner: I, cipher_config: SqlCipherConfig, } impl CipheredConnectionStrategy where CS: ConnectionStrategy, { /// Create a new SQLCipher connection that inherits another connection strategy. pub fn new(strategy: CS, cipher_config: SqlCipherConfig) -> CipheredConnectionStrategy { CipheredConnectionStrategy { inner: strategy, cipher_config, } } } impl ConnectionStrategy for CipheredConnectionStrategy where I: ConnectionStrategy, { fn make_connection(&self) -> Result { if self.cipher_config.plaintext_header_size > 0 && !matches!(self.cipher_config.key, SqlCipherKey::RawKeyWithSalt(_)) { return Err(SqLiteDataStorageError::SqlCipherKeyInvalidWithHeader); } let connection = self.inner.make_connection()?; connection .pragma_update( None, "key", self.cipher_config.key.to_key_pragma_value().as_str(), ) .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?; connection .pragma_update( None, "cipher_plaintext_header_size", self.cipher_config.plaintext_header_size, ) .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?; // Verify that the database is keyed correctly connection .query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(())) .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?; Ok(connection) } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use tempfile::NamedTempFile; use crate::cipher::SqlCipherConfig; use crate::connection_strategy::{ConnectionStrategy, MemoryStrategy}; use crate::test_utils::gen_rand_bytes; use crate::{connection_strategy::FileConnectionStrategy, SqLiteDataStorageError}; use super::{CipheredConnectionStrategy, SqlCipherKey}; fn sql_cipher_test(config: SqlCipherConfig) { let temp_file = NamedTempFile::new().unwrap(); let mut sqlcipher_strategy = CipheredConnectionStrategy::new(FileConnectionStrategy::new(temp_file.path()), config); // Test first connection let connection = sqlcipher_strategy.make_connection().unwrap(); connection.execute("CREATE TABLE test(item)", []).unwrap(); // Test reopen for another connection sqlcipher_strategy.make_connection().unwrap(); // Verify plaintext header size assert_eq!( connection .pragma_query_value(None, "cipher_plaintext_header_size", |row| { row.get::<_, String>(0) }) .unwrap(), sqlcipher_strategy .cipher_config .plaintext_header_size .to_string() ); // Test incorrect key sqlcipher_strategy.cipher_config.key = match sqlcipher_strategy.cipher_config.key { SqlCipherKey::Passphrase(_) => SqlCipherKey::Passphrase("incorrect".to_string()), SqlCipherKey::RawKey(_) => SqlCipherKey::RawKey(gen_rand_bytes(32).try_into().unwrap()), SqlCipherKey::RawKeyWithSalt(_) => { SqlCipherKey::RawKeyWithSalt(gen_rand_bytes(48).try_into().unwrap()) } }; assert_matches!( sqlcipher_strategy.make_connection(), Err(SqLiteDataStorageError::SqlEngineError(_)) ); } #[test] fn sql_cipher_passphrase() { let config = SqlCipherConfig::new(SqlCipherKey::Passphrase("correct".to_string())); sql_cipher_test(config); } #[test] fn sql_cipher_raw_key() { let config = SqlCipherConfig::new(SqlCipherKey::RawKey(gen_rand_bytes(32).try_into().unwrap())); sql_cipher_test(config); } #[test] fn sql_cipher_raw_key_salt() { let config = SqlCipherConfig::new(SqlCipherKey::RawKeyWithSalt( gen_rand_bytes(48).try_into().unwrap(), )); sql_cipher_test(config); } #[test] fn sql_cipher_plaintext_header() { let config = SqlCipherConfig::new(SqlCipherKey::RawKeyWithSalt( gen_rand_bytes(48).try_into().unwrap(), )) .with_plaintext_header(32); sql_cipher_test(config); } #[test] fn sql_cipher_invalid_key_plaintext_header() { let config = SqlCipherConfig::new(SqlCipherKey::Passphrase("correct".to_string())) .with_plaintext_header(32); let res = CipheredConnectionStrategy::new(MemoryStrategy, config).make_connection(); assert_matches!( res, Err(SqLiteDataStorageError::SqlCipherKeyInvalidWithHeader) ); } }