revert(sqlite): remove RC4 Legacy database support
This commit is contained in:
19 files changed
+49
-187
No files matched your search
@@ -9,7 +9,7 @@ default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system
|
||||
duckdb-sidecar = []
|
||||
dynamodb = ["dep:aws-sdk-dynamodb"]
|
||||
mq-admin = ["dep:rumqttc"]
|
||||
sqlite-sqlcipher = []
|
||||
sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
openapi = ["dep:utoipa"]
|
||||
system-fonts = ["font-kit"]
|
||||
|
||||
@@ -42,8 +42,7 @@ tokio-postgres-rustls = "0.13"
|
||||
postgres-openssl = "0.5"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
webpki-roots = "0.26"
|
||||
rusqlite = { version = "0.32", default-features = false, features = ["load_extension", "backup", "functions", "column_decltype"] }
|
||||
libsqlite3-hotbundle = { git = "https://github.com/nikescar/libsqlite3-hotbundle.git", rev = "a208c7ef1447a02bf5ecb8c6a297aca012d41244", features = ["cipher-sqlcipher"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled", "load_extension", "backup", "functions", "column_decltype"] }
|
||||
mysql_async = { version = "0.37", default-features = false, features = ["default-rustls", "client_ed25519", "chrono", "rust_decimal"] }
|
||||
sqlparser = { version = "0.62.0", features = ["visitor"] }
|
||||
redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tls-rustls-insecure", "tokio-rustls-comp", "sentinel", "cluster-async"] }
|
||||
|
||||
@@ -20,8 +20,6 @@ use crate::types::{
|
||||
};
|
||||
|
||||
const SQLITE_DATABASE_HEADER: &[u8; 16] = b"SQLite format 3\0";
|
||||
// Probe the common modern and legacy sizes first, then the remaining supported powers of two.
|
||||
const SQLITE_LEGACY_PAGE_SIZES: &[i64] = &[4096, 1024, 512, 2048, 8192, 16384, 32768, 65536];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteHandle {
|
||||
@@ -74,7 +72,7 @@ pub fn validate_persistent_attachments(main_path: &str, cipher_key: &str, has_at
|
||||
}
|
||||
if !cipher_key.is_empty() {
|
||||
return Err(
|
||||
"Persistent SQLite attachments are unavailable for encrypted connections because each attached database requires an explicit key."
|
||||
"Persistent SQLite attachments are unavailable for SQLCipher connections because each attached database requires an explicit key."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -138,7 +136,7 @@ fn open_sqlite_handle(
|
||||
) -> Result<SqliteHandle, String> {
|
||||
let is_memory = is_memory_database_path(path);
|
||||
let encrypted = cipher_key.as_deref().is_some_and(|key| !key.is_empty());
|
||||
ensure_sqlite_encryption_available(encrypted)?;
|
||||
ensure_sqlcipher_available(encrypted)?;
|
||||
if !is_memory && !create_if_missing {
|
||||
validate_file_path(path, is_network_path)?;
|
||||
}
|
||||
@@ -150,25 +148,12 @@ fn open_sqlite_handle(
|
||||
validate_existing_sqlite_file(path)?;
|
||||
}
|
||||
|
||||
let cipher_attempts = if !encrypted {
|
||||
vec![SqliteCipherAttempt::Plain]
|
||||
} else if create_if_missing {
|
||||
vec![SqliteCipherAttempt::SqlCipher(4)]
|
||||
} else {
|
||||
let mut attempts = vec![
|
||||
SqliteCipherAttempt::SqlCipher(4),
|
||||
SqliteCipherAttempt::SqlCipher(3),
|
||||
SqliteCipherAttempt::SqlCipher(2),
|
||||
SqliteCipherAttempt::SqlCipher(1),
|
||||
];
|
||||
attempts.extend(SQLITE_LEGACY_PAGE_SIZES.iter().copied().map(SqliteCipherAttempt::Rc4Legacy));
|
||||
attempts
|
||||
};
|
||||
let sqlcipher_attempts: &[Option<i64>] = if encrypted { &[None, Some(3), Some(2), Some(1)] } else { &[None] };
|
||||
let mut unlock_error: Option<String> = None;
|
||||
|
||||
for attempt in cipher_attempts {
|
||||
for compatibility in sqlcipher_attempts {
|
||||
let conn = open_sqlite_connection(path, create_if_missing)?;
|
||||
if let Err(err) = apply_sqlite_cipher_key(&conn, cipher_key.as_deref(), attempt) {
|
||||
if let Err(err) = apply_sqlcipher_key(&conn, cipher_key.as_deref(), *compatibility) {
|
||||
unlock_error = Some(err);
|
||||
continue;
|
||||
}
|
||||
@@ -179,15 +164,7 @@ fn open_sqlite_handle(
|
||||
return Ok(SqliteHandle { conn: Arc::new(Mutex::new(conn)) });
|
||||
}
|
||||
|
||||
Err(unlock_error.unwrap_or_else(|| "Encrypted SQLite database unlock failed.".to_string()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[cfg_attr(not(feature = "sqlite-sqlcipher"), allow(dead_code))]
|
||||
enum SqliteCipherAttempt {
|
||||
Plain,
|
||||
SqlCipher(i64),
|
||||
Rc4Legacy(i64),
|
||||
Err(unlock_error.unwrap_or_else(|| "SQLCipher database unlock failed.".to_string()))
|
||||
}
|
||||
|
||||
fn open_sqlite_connection(path: &str, create_if_missing: bool) -> Result<Connection, String> {
|
||||
@@ -217,61 +194,48 @@ fn sqlite_cipher_key(cipher_key: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite-sqlcipher")]
|
||||
fn ensure_sqlite_encryption_available(_encrypted: bool) -> Result<(), String> {
|
||||
fn ensure_sqlcipher_available(_encrypted: bool) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sqlite-sqlcipher"))]
|
||||
fn ensure_sqlite_encryption_available(encrypted: bool) -> Result<(), String> {
|
||||
fn ensure_sqlcipher_available(encrypted: bool) -> Result<(), String> {
|
||||
if encrypted {
|
||||
Err("Encrypted SQLite support is not compiled in this build. Rebuild with the sqlite-sqlcipher feature."
|
||||
.to_string())
|
||||
Err("SQLCipher support is not compiled in this build. Rebuild with the sqlite-sqlcipher feature.".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite-sqlcipher")]
|
||||
fn apply_sqlite_cipher_key(
|
||||
conn: &Connection,
|
||||
cipher_key: Option<&str>,
|
||||
attempt: SqliteCipherAttempt,
|
||||
) -> Result<(), String> {
|
||||
fn apply_sqlcipher_key(conn: &Connection, cipher_key: Option<&str>, compatibility: Option<i64>) -> Result<(), String> {
|
||||
let Some(cipher_key) = cipher_key.filter(|key| !key.is_empty()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match attempt {
|
||||
SqliteCipherAttempt::Plain => {}
|
||||
SqliteCipherAttempt::SqlCipher(compatibility) => {
|
||||
conn.pragma_update(None, "cipher", "sqlcipher").map_err(|e| format!("SQLite cipher setup failed: {e}"))?;
|
||||
conn.pragma_update(None, "legacy", compatibility)
|
||||
.map_err(|e| format!("SQLCipher compatibility setup failed: {e}"))?;
|
||||
}
|
||||
SqliteCipherAttempt::Rc4Legacy(page_size) => {
|
||||
conn.pragma_update(None, "cipher", "rc4").map_err(|e| format!("SQLite cipher setup failed: {e}"))?;
|
||||
conn.pragma_update(None, "legacy", 1).map_err(|e| format!("RC4 compatibility setup failed: {e}"))?;
|
||||
conn.pragma_update(None, "legacy_page_size", page_size)
|
||||
.map_err(|e| format!("RC4 legacy page size setup failed: {e}"))?;
|
||||
}
|
||||
// SQLCipher requires the key before the first schema read; the verification
|
||||
// query turns wrong keys into an immediate connection error.
|
||||
conn.pragma_update(None, "key", cipher_key).map_err(|e| format!("SQLCipher key setup failed: {e}"))?;
|
||||
if let Some(compatibility) = compatibility {
|
||||
conn.pragma_update(None, "cipher_compatibility", compatibility)
|
||||
.map_err(|e| format!("SQLCipher compatibility setup failed: {e}"))?;
|
||||
}
|
||||
conn.pragma_update(None, "key", cipher_key).map_err(|e| format!("SQLite encryption key setup failed: {e}"))?;
|
||||
verify_sqlite_encryption_key(conn)
|
||||
.map_err(|e| format!("Encrypted SQLite database unlock failed. Check the password/key and cipher: {e}"))?;
|
||||
verify_sqlcipher_key(conn)
|
||||
.map_err(|e| format!("SQLCipher database unlock failed. Check the SQLite password/key and file type: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sqlite-sqlcipher"))]
|
||||
fn apply_sqlite_cipher_key(
|
||||
fn apply_sqlcipher_key(
|
||||
_conn: &Connection,
|
||||
_cipher_key: Option<&str>,
|
||||
_attempt: SqliteCipherAttempt,
|
||||
_compatibility: Option<i64>,
|
||||
) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite-sqlcipher")]
|
||||
fn verify_sqlite_encryption_key(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||
fn verify_sqlcipher_key(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||
conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
|
||||
}
|
||||
|
||||
@@ -604,7 +568,7 @@ mod tests {
|
||||
assert!(memory_error.contains("in-memory main database"), "{memory_error}");
|
||||
|
||||
let cipher_error = validate_persistent_attachments("/tmp/main.sqlite", "secret", true).unwrap_err();
|
||||
assert!(cipher_error.contains("encrypted"), "{cipher_error}");
|
||||
assert!(cipher_error.contains("SQLCipher"), "{cipher_error}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -759,7 +723,7 @@ mod tests {
|
||||
Ok(_) => panic!("wrong key must fail"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert!(wrong_key.contains("Encrypted SQLite database unlock failed"));
|
||||
assert!(wrong_key.contains("SQLCipher database unlock failed"));
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
@@ -774,9 +738,8 @@ mod tests {
|
||||
let conn =
|
||||
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE)
|
||||
.expect("create legacy-compatible encrypted sqlite");
|
||||
conn.pragma_update(None, "cipher", "sqlcipher").expect("select SQLCipher");
|
||||
conn.pragma_update(None, "legacy", 3).expect("set SQLCipher compatibility");
|
||||
conn.pragma_update(None, "key", key).expect("set SQLCipher key");
|
||||
conn.pragma_update(None, "cipher_compatibility", 3).expect("set SQLCipher compatibility");
|
||||
conn.execute_batch("CREATE TABLE t (name TEXT); INSERT INTO t VALUES ('legacy');")
|
||||
.expect("write legacy-compatible encrypted sqlite");
|
||||
}
|
||||
@@ -791,48 +754,6 @@ mod tests {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite-sqlcipher")]
|
||||
#[tokio::test]
|
||||
async fn encrypted_sqlite_key_opens_and_updates_1024_page_rc4_legacy_database() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-rc4-legacy-{}.db", uuid::Uuid::new_v4()));
|
||||
let key = "123456";
|
||||
let page_size = 1024;
|
||||
|
||||
{
|
||||
let conn =
|
||||
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE)
|
||||
.expect("create RC4-compatible encrypted sqlite");
|
||||
conn.pragma_update(None, "cipher", "rc4").expect("select RC4");
|
||||
conn.pragma_update(None, "legacy", 1).expect("set RC4 legacy mode");
|
||||
conn.pragma_update(None, "legacy_page_size", page_size).expect("set RC4 legacy page size");
|
||||
conn.pragma_update(None, "key", key).expect("set RC4 key");
|
||||
conn.execute_batch("CREATE TABLE t (name TEXT); INSERT INTO t VALUES ('navicat-compatible');")
|
||||
.expect("write RC4-compatible encrypted sqlite");
|
||||
}
|
||||
|
||||
{
|
||||
let reopened = connect_path_with_cipher_key_and_extensions(path.to_str().unwrap(), key, Vec::new())
|
||||
.await
|
||||
.expect("open RC4-compatible encrypted sqlite");
|
||||
let result = execute_query(&reopened, "SELECT name FROM t").await.expect("read RC4 database");
|
||||
assert_eq!(result.rows[0][0], serde_json::json!("navicat-compatible"));
|
||||
execute_query(&reopened, "INSERT INTO t VALUES ('written-by-dbx')").await.expect("update RC4 database");
|
||||
}
|
||||
|
||||
let conn = Connection::open(&path).expect("reopen RC4 database independently");
|
||||
conn.pragma_update(None, "cipher", "rc4").expect("select RC4");
|
||||
conn.pragma_update(None, "legacy", 1).expect("set RC4 legacy mode");
|
||||
conn.pragma_update(None, "legacy_page_size", page_size).expect("set RC4 legacy page size");
|
||||
conn.pragma_update(None, "key", key).expect("set RC4 key");
|
||||
let reopened_page_size: i64 =
|
||||
conn.pragma_query_value(None, "page_size", |row| row.get(0)).expect("read page size");
|
||||
assert_eq!(reopened_page_size, page_size);
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0)).expect("verify RC4 writes");
|
||||
assert_eq!(count, 2);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sqlite-sqlcipher"))]
|
||||
#[tokio::test]
|
||||
async fn sqlcipher_key_requires_sqlcipher_feature() {
|
||||
@@ -844,7 +765,7 @@ mod tests {
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
assert!(err.contains("Encrypted SQLite support is not compiled"));
|
||||
assert!(err.contains("SQLCipher support is not compiled"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user