mls_rs_provider_sqlite/
cipher.rs1use crate::connection_strategy::ConnectionStrategy;
6use crate::SqLiteDataStorageError;
7use rusqlite::Connection;
8
9use hex::ToHex;
10use zeroize::{ZeroizeOnDrop, Zeroizing};
11
12#[allow(dead_code)]
13#[derive(Debug, ZeroizeOnDrop, Clone)]
14pub enum SqlCipherKey {
16 Passphrase(String),
18 RawKey([u8; 32]),
20 RawKeyWithSalt([u8; 48]),
22}
23
24fn blob_string_repr(val: &[u8]) -> String {
25 format!("x'{}'", val.encode_hex_upper::<String>())
26}
27
28impl SqlCipherKey {
29 fn to_key_pragma_value(&self) -> Zeroizing<String> {
30 Zeroizing::new(match self {
31 SqlCipherKey::Passphrase(pass) => pass.clone(),
32 SqlCipherKey::RawKey(key) => blob_string_repr(key.as_slice()),
33 SqlCipherKey::RawKeyWithSalt(key) => blob_string_repr(key.as_slice()),
34 })
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct SqlCipherConfig {
41 key: SqlCipherKey,
42 plaintext_header_size: u8,
43}
44
45impl SqlCipherConfig {
46 pub fn new(key: SqlCipherKey) -> SqlCipherConfig {
48 SqlCipherConfig {
49 key,
50 plaintext_header_size: 0,
51 }
52 }
53
54 pub fn with_plaintext_header(self, size: u8) -> SqlCipherConfig {
56 SqlCipherConfig {
57 plaintext_header_size: size,
58 ..self
59 }
60 }
61}
62
63pub struct CipheredConnectionStrategy<I>
65where
66 I: ConnectionStrategy,
67{
68 inner: I,
69 cipher_config: SqlCipherConfig,
70}
71
72impl<CS> CipheredConnectionStrategy<CS>
73where
74 CS: ConnectionStrategy,
75{
76 pub fn new(strategy: CS, cipher_config: SqlCipherConfig) -> CipheredConnectionStrategy<CS> {
78 CipheredConnectionStrategy {
79 inner: strategy,
80 cipher_config,
81 }
82 }
83}
84
85impl<I> ConnectionStrategy for CipheredConnectionStrategy<I>
86where
87 I: ConnectionStrategy,
88{
89 fn make_connection(&self) -> Result<Connection, SqLiteDataStorageError> {
90 if self.cipher_config.plaintext_header_size > 0
91 && !matches!(self.cipher_config.key, SqlCipherKey::RawKeyWithSalt(_))
92 {
93 return Err(SqLiteDataStorageError::SqlCipherKeyInvalidWithHeader);
94 }
95
96 let connection = self.inner.make_connection()?;
97
98 connection
99 .pragma_update(
100 None,
101 "key",
102 self.cipher_config.key.to_key_pragma_value().as_str(),
103 )
104 .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?;
105
106 connection
107 .pragma_update(
108 None,
109 "cipher_plaintext_header_size",
110 self.cipher_config.plaintext_header_size,
111 )
112 .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?;
113
114 connection
116 .query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
117 .map_err(|e| SqLiteDataStorageError::SqlEngineError(e.into()))?;
118
119 Ok(connection)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use assert_matches::assert_matches;
126 use tempfile::NamedTempFile;
127
128 use crate::cipher::SqlCipherConfig;
129 use crate::connection_strategy::{ConnectionStrategy, MemoryStrategy};
130 use crate::test_utils::gen_rand_bytes;
131 use crate::{connection_strategy::FileConnectionStrategy, SqLiteDataStorageError};
132
133 use super::{CipheredConnectionStrategy, SqlCipherKey};
134
135 fn sql_cipher_test(config: SqlCipherConfig) {
136 let temp_file = NamedTempFile::new().unwrap();
137
138 let mut sqlcipher_strategy =
139 CipheredConnectionStrategy::new(FileConnectionStrategy::new(temp_file.path()), config);
140
141 let connection = sqlcipher_strategy.make_connection().unwrap();
143 connection.execute("CREATE TABLE test(item)", []).unwrap();
144
145 sqlcipher_strategy.make_connection().unwrap();
147
148 assert_eq!(
150 connection
151 .pragma_query_value(None, "cipher_plaintext_header_size", |row| {
152 row.get::<_, String>(0)
153 })
154 .unwrap(),
155 sqlcipher_strategy
156 .cipher_config
157 .plaintext_header_size
158 .to_string()
159 );
160
161 sqlcipher_strategy.cipher_config.key = match sqlcipher_strategy.cipher_config.key {
163 SqlCipherKey::Passphrase(_) => SqlCipherKey::Passphrase("incorrect".to_string()),
164 SqlCipherKey::RawKey(_) => SqlCipherKey::RawKey(gen_rand_bytes(32).try_into().unwrap()),
165 SqlCipherKey::RawKeyWithSalt(_) => {
166 SqlCipherKey::RawKeyWithSalt(gen_rand_bytes(48).try_into().unwrap())
167 }
168 };
169
170 assert_matches!(
171 sqlcipher_strategy.make_connection(),
172 Err(SqLiteDataStorageError::SqlEngineError(_))
173 );
174 }
175
176 #[test]
177 fn sql_cipher_passphrase() {
178 let config = SqlCipherConfig::new(SqlCipherKey::Passphrase("correct".to_string()));
179
180 sql_cipher_test(config);
181 }
182
183 #[test]
184 fn sql_cipher_raw_key() {
185 let config =
186 SqlCipherConfig::new(SqlCipherKey::RawKey(gen_rand_bytes(32).try_into().unwrap()));
187
188 sql_cipher_test(config);
189 }
190
191 #[test]
192 fn sql_cipher_raw_key_salt() {
193 let config = SqlCipherConfig::new(SqlCipherKey::RawKeyWithSalt(
194 gen_rand_bytes(48).try_into().unwrap(),
195 ));
196
197 sql_cipher_test(config);
198 }
199
200 #[test]
201 fn sql_cipher_plaintext_header() {
202 let config = SqlCipherConfig::new(SqlCipherKey::RawKeyWithSalt(
203 gen_rand_bytes(48).try_into().unwrap(),
204 ))
205 .with_plaintext_header(32);
206
207 sql_cipher_test(config);
208 }
209
210 #[test]
211 fn sql_cipher_invalid_key_plaintext_header() {
212 let config = SqlCipherConfig::new(SqlCipherKey::Passphrase("correct".to_string()))
213 .with_plaintext_header(32);
214
215 let res = CipheredConnectionStrategy::new(MemoryStrategy, config).make_connection();
216
217 assert_matches!(
218 res,
219 Err(SqLiteDataStorageError::SqlCipherKeyInvalidWithHeader)
220 );
221 }
222}