mls_rs_provider_sqlite/
cipher.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5use 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)]
14/// Representation of a SQLCipher key used to unlock a database.
15pub enum SqlCipherKey {
16    /// Passphrase based key.
17    Passphrase(String),
18    /// Raw key material without a salt value.
19    RawKey([u8; 32]),
20    /// Raw key material with a salt value.
21    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)]
39/// SQLCipher connection config.
40pub struct SqlCipherConfig {
41    key: SqlCipherKey,
42    plaintext_header_size: u8,
43}
44
45impl SqlCipherConfig {
46    /// Create a new config with a specific key.
47    pub fn new(key: SqlCipherKey) -> SqlCipherConfig {
48        SqlCipherConfig {
49            key,
50            plaintext_header_size: 0,
51        }
52    }
53
54    /// Adjust the plaintext header size.
55    pub fn with_plaintext_header(self, size: u8) -> SqlCipherConfig {
56        SqlCipherConfig {
57            plaintext_header_size: size,
58            ..self
59        }
60    }
61}
62
63/// Encrypted database connection with SQLCipher.
64pub 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    /// Create a new SQLCipher connection that inherits another connection strategy.
77    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        // Verify that the database is keyed correctly
115        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        // Test first connection
142        let connection = sqlcipher_strategy.make_connection().unwrap();
143        connection.execute("CREATE TABLE test(item)", []).unwrap();
144
145        // Test reopen for another connection
146        sqlcipher_strategy.make_connection().unwrap();
147
148        // Verify plaintext header size
149        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        // Test incorrect key
162        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}