revert(sqlite): remove RC4 Legacy database support

This commit is contained in:
github-actions[bot] committed 2026-08-26 02:46:04 +08:00
1 parent d362c8efb8
commit 706334434a
19 files changed
+49 -187

No files matched your search

+2 -3
View File
@@ -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"] }
+25 -104
View File
@@ -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]