Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Szymek156/issue2009 #2014

Merged
merged 4 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,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"] }
szymek156 marked this conversation as resolved.
Show resolved Hide resolved

#
# Any
#
Expand Down Expand Up @@ -198,6 +202,11 @@ name = "sqlite-derives"
path = "tests/sqlite/derives.rs"
required-features = ["sqlite", "macros"]

[[test]]
name = "sqlcipher"
path = "tests/sqlite/sqlcipher.rs"
required-features = ["sqlite"]

[[test]]
name = "sqlite-test-attr"
path = "tests/sqlite/test-attr.rs"
Expand Down
44 changes: 40 additions & 4 deletions sqlx-core/src/sqlite/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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);
Expand Down Expand Up @@ -284,9 +320,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\<B then B>A.
/// > If A==B then B==A.
/// > If A==B and B==C then A==C.
/// > If A\<B then B>A.
/// > If A<B and B<C then A<C.
/// >
/// > If a collating function fails any of the above constraints and that collating function is
Expand Down Expand Up @@ -328,7 +364,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
Expand Down
202 changes: 202 additions & 0 deletions tests/sqlite/sqlcipher.rs
Original file line number Diff line number Diff line change
@@ -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<SqliteQueryResult> {
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(())
}