mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 06:24:16 +00:00
Szymek156/issue2009 (#2014)
* 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. * Added tests for SQLCipher functionality * remove default-features from libsqlite3-sys when building from dev-dependencies Co-authored-by: Szymon Zimnowoda <szimnowoda.memri@gmail.com>
This commit is contained in:
parent
728717f032
commit
c931cab95f
3 changed files with 251 additions and 4 deletions
|
@ -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 = "*", features = ["bundled-sqlcipher"] }
|
||||
|
||||
#
|
||||
# Any
|
||||
#
|
||||
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
|
202
tests/sqlite/sqlcipher.rs
Normal file
202
tests/sqlite/sqlcipher.rs
Normal file
|
@ -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(())
|
||||
}
|
Loading…
Reference in a new issue