From d9b8f81bcad7025173aa57b00f90d020fb271e20 Mon Sep 17 00:00:00 2001 From: Szymon Zimnowoda Date: Mon, 1 Aug 2022 10:43:48 +0200 Subject: [PATCH 1/3] SQLite: Execute SQLCipher pragmas as very first operations on the database SQLCipher requires, apart from 'key' pragma also other cipher-related to be executed before read/write to the database. --- sqlx-core/src/sqlite/options/mod.rs | 44 ++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index b3bb53e5a4..5090fd9eb5 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -101,6 +101,42 @@ impl SqliteConnectOptions { // SQLCipher special case: if the `key` pragma is set, it must be executed first. pragmas.insert("key".into(), None); + // Other SQLCipher pragmas that has to be after the key, but before any other operation on the database. + // https://www.zetetic.net/sqlcipher/sqlcipher-api/ + + // Bytes of the database file that is not encrypted + // Default for SQLCipher v4 is 0 + // If greater than zero 'cipher_salt' pragma must be also defined + pragmas.insert("cipher_plaintext_header_size".into(), None); + + // Allows to provide salt manually + // By default SQLCipher sets salt automatically, use only in conjunction with + // 'cipher_plaintext_header_size' pragma + pragmas.insert("cipher_salt".into(), None); + + // Number of iterations used in PBKDF2 key derivation. + // Default for SQLCipher v4 is 256000 + pragmas.insert("kdf_iter".into(), None); + + // Define KDF algorithm to be used. + // Default for SQLCipher v4 is PBKDF2_HMAC_SHA512. + pragmas.insert("cipher_kdf_algorithm".into(), None); + + // Enable or disable HMAC functionality. + // Default for SQLCipher v4 is 1. + pragmas.insert("cipher_use_hmac".into(), None); + + // Set default encryption settings depending on the version 1,2,3, or 4. + pragmas.insert("cipher_compatibility".into(), None); + + // Page size of encrypted database. + // Default for SQLCipher v4 is 4096. + pragmas.insert("cipher_page_size".into(), None); + + // Choose algorithm used for HMAC. + // Default for SQLCipher v4 is HMAC_SHA512. + pragmas.insert("cipher_hmac_algorithm".into(), None); + // Normally, page_size must be set before any other action on the database. // Defaults to 4096 for new databases. pragmas.insert("page_size".into(), None); @@ -282,9 +318,9 @@ impl SqliteConnectOptions { /// Note this excerpt: /// > The collating function must obey the following properties for all strings A, B, and C: /// > - /// > If A==B then B==A. - /// > If A==B and B==C then A==C. - /// > If A\A. + /// > If A==B then B==A. + /// > If A==B and B==C then A==C. + /// > If A\A. /// > If A /// > If a collating function fails any of the above constraints and that collating function is @@ -326,7 +362,7 @@ impl SqliteConnectOptions { /// ### Note /// Setting this to `true` may help if you are getting access violation errors or segmentation /// faults, but will also incur a significant performance penalty. You should leave this - /// set to `false` if at all possible. + /// set to `false` if at all possible. /// /// If you do end up needing to set this to `true` for some reason, please /// [open an issue](https://github.com/launchbadge/sqlx/issues/new/choose) as this may indicate From 8a97782c160a896d417c35aa8a893d0c4c7ab2d9 Mon Sep 17 00:00:00 2001 From: Szymon Zimnowoda Date: Mon, 1 Aug 2022 23:08:35 +0200 Subject: [PATCH 2/3] Added tests for SQLCipher functionality --- Cargo.toml | 9 ++ tests/sqlite/sqlcipher.rs | 202 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 tests/sqlite/sqlcipher.rs diff --git a/Cargo.toml b/Cargo.toml index 9938ce094e..201a46bd05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,10 @@ url = "2.2.2" rand = "0.8.4" rand_xoshiro = "0.6.0" hex = "0.4.3" +tempdir = "0.3.7" +# Needed to test SQLCipher +libsqlite3-sys = { version = "*", default-features = false, features = ["bundled-sqlcipher"] } + # # Any # @@ -206,6 +210,11 @@ name = "sqlite-derives" path = "tests/sqlite/derives.rs" required-features = ["sqlite", "macros"] +[[test]] +name = "sqlcipher" +path = "tests/sqlite/sqlcipher.rs" +required-features = ["sqlite"] + # # MySQL # diff --git a/tests/sqlite/sqlcipher.rs b/tests/sqlite/sqlcipher.rs new file mode 100644 index 0000000000..0a2a4499ea --- /dev/null +++ b/tests/sqlite/sqlcipher.rs @@ -0,0 +1,202 @@ +use std::str::FromStr; + +use sqlx::sqlite::SqliteQueryResult; +use sqlx::{query, Connection, SqliteConnection}; +use sqlx::{sqlite::SqliteConnectOptions, ConnectOptions}; +use sqlx_rt::fs::File; +use tempdir::TempDir; + +async fn new_db_url() -> anyhow::Result<(String, TempDir)> { + let dir = TempDir::new("sqlcipher_test")?; + let filepath = dir.path().join("database.sqlite3"); + + // Touch the file, so DB driver will not complain it does not exist + File::create(filepath.as_path()).await?; + + Ok((format!("sqlite://{}", filepath.display()), dir)) +} + +async fn fill_db(conn: &mut SqliteConnection) -> anyhow::Result { + conn.transaction(|tx| { + Box::pin(async move { + query( + " + CREATE TABLE Company( + Id INT PRIMARY KEY NOT NULL, + Name TEXT NOT NULL, + Salary REAL + ); + ", + ) + .execute(&mut *tx) + .await?; + + query( + r#" + INSERT INTO Company(Id, Name, Salary) + VALUES + (1, "aaa", 111), + (2, "bbb", 222) + "#, + ) + .execute(tx) + .await + }) + }) + .await + .map_err(|e| e.into()) +} + +#[sqlx_macros::test] +async fn it_encrypts() -> anyhow::Result<()> { + let (url, _dir) = new_db_url().await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "the_password") + .connect() + .await?; + + fill_db(&mut conn).await?; + + // Create another connection without key, query should fail + let mut conn = SqliteConnectOptions::from_str(&url)?.connect().await?; + + assert!(conn + .transaction(|tx| { + Box::pin(async move { query("SELECT * FROM Company;").fetch_all(tx).await }) + }) + .await + .is_err()); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_can_store_and_read_encrypted_data() -> anyhow::Result<()> { + let (url, _dir) = new_db_url().await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "the_password") + .connect() + .await?; + + fill_db(&mut conn).await?; + + // Create another connection with valid key + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "the_password") + .connect() + .await?; + + let result = conn + .transaction(|tx| { + Box::pin(async move { query("SELECT * FROM Company;").fetch_all(tx).await }) + }) + .await?; + + assert!(result.len() > 0); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_if_password_is_incorrect() -> anyhow::Result<()> { + let (url, _dir) = new_db_url().await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "the_password") + .connect() + .await?; + + fill_db(&mut conn).await?; + + // Connection with invalid key should not allow to execute queries + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "BADBADBAD") + .connect() + .await?; + + assert!(conn + .transaction(|tx| { + Box::pin(async move { query("SELECT * FROM Company;").fetch_all(tx).await }) + }) + .await + .is_err()); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_honors_order_of_encryption_pragmas() -> anyhow::Result<()> { + let (url, _dir) = new_db_url().await?; + + // Make call of cipher configuration mixed with other pragmas, + // it should have no effect, encryption related pragmas should be + // executed first and allow to establish valid connection + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("cipher_kdf_algorithm", "PBKDF2_HMAC_SHA1") + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .pragma("cipher_page_size", "1024") + .pragma("key", "the_password") + .foreign_keys(true) + .pragma("kdf_iter", "64000") + .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental) + .pragma("cipher_hmac_algorithm", "HMAC_SHA1") + .connect() + .await?; + + fill_db(&mut conn).await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("dummy", "pragma") + // The cipher configuration set on first connection is + // version 3 of SQLCipher, so for second it's enough to set + // the compatibility mode. + .pragma("cipher_compatibility", "3") + .pragma("key", "the_password") + .connect() + .await?; + + let result = conn + .transaction(|tx| { + Box::pin(async move { query("SELECT * FROM COMPANY;").fetch_all(tx).await }) + }) + .await?; + + assert!(result.len() > 0); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_allows_to_rekey_the_db() -> anyhow::Result<()> { + let (url, _dir) = new_db_url().await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("key", "the_password") + .connect() + .await?; + + fill_db(&mut conn).await?; + + // The 'pragma rekey' can be called at any time + query("PRAGMA rekey = new_password;") + .execute(&mut conn) + .await?; + + let mut conn = SqliteConnectOptions::from_str(&url)? + .pragma("dummy", "pragma") + .pragma("key", "new_password") + .connect() + .await?; + + let result = conn + .transaction(|tx| { + Box::pin(async move { query("SELECT * FROM COMPANY;").fetch_all(tx).await }) + }) + .await?; + + assert!(result.len() > 0); + + Ok(()) +} From 5b6d80a0e4ac450fe655d9d8d7392cf03b454b9c Mon Sep 17 00:00:00 2001 From: Szymon Zimnowoda Date: Thu, 4 Aug 2022 09:14:29 +0200 Subject: [PATCH 3/3] remove default-features from libsqlite3-sys when building from dev-dependencies --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 647fb3d4e6..41d05245c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,7 +148,7 @@ rand_xoshiro = "0.6.0" hex = "0.4.3" tempdir = "0.3.7" # Needed to test SQLCipher -libsqlite3-sys = { version = "*", default-features = false, features = ["bundled-sqlcipher"] } +libsqlite3-sys = { version = "*", features = ["bundled-sqlcipher"] } # # Any