From 706334434a8a523098af53c4e2fc6e87ac29bd95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Aug 2026 02:46:04 +0800 Subject: [PATCH] revert(sqlite): remove RC4 Legacy database support --- Cargo.lock | 11 +- agents/drivers/duckdb/Cargo.lock | 10 +- apps/desktop/src/App.vue | 2 +- apps/desktop/src/composables/useFileDrop.ts | 4 +- apps/desktop/src/i18n/locales/en.ts | 2 +- apps/desktop/src/i18n/locales/es.ts | 2 +- apps/desktop/src/i18n/locales/it.ts | 2 +- apps/desktop/src/i18n/locales/ja.ts | 2 +- apps/desktop/src/i18n/locales/ko.ts | 2 +- apps/desktop/src/i18n/locales/pt-BR.ts | 2 +- apps/desktop/src/i18n/locales/zh-CN.ts | 2 +- apps/desktop/src/i18n/locales/zh-TW.ts | 2 +- .../__tests__/connectionErrorHints.spec.ts | 10 +- .../lib/connection/connectionErrorHints.ts | 5 - .../connectionStore.savePassword.spec.ts | 29 ---- apps/desktop/src/stores/connectionStore.ts | 11 +- crates/dbx-core/Cargo.toml | 5 +- crates/dbx-core/src/db/sqlite.rs | 129 ++++-------------- src-tauri/src/commands/connection.rs | 4 +- 19 files changed, 49 insertions(+), 187 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79db0a0a67..d6cfa9a9b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2096,7 +2096,6 @@ dependencies = [ "insta", "json5", "jsonwebtoken", - "libsqlite3-hotbundle", "log", "minijinja", "mongodb", @@ -4612,20 +4611,14 @@ dependencies = [ "libc", ] -[[package]] -name = "libsqlite3-hotbundle" -version = "1.510300.0" -source = "git+https://github.com/nikescar/libsqlite3-hotbundle.git?rev=a208c7ef1447a02bf5ecb8c6a297aca012d41244#a208c7ef1447a02bf5ecb8c6a297aca012d41244" -dependencies = [ - "cc", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", + "openssl-sys", "pkg-config", "vcpkg", ] diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index ed514f3981..470ef9e15a 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -1252,7 +1252,6 @@ dependencies = [ "insta", "json5", "jsonwebtoken", - "libsqlite3-hotbundle", "log", "minijinja", "mongodb", @@ -2835,20 +2834,13 @@ dependencies = [ "libc", ] -[[package]] -name = "libsqlite3-hotbundle" -version = "1.510300.0" -source = "git+https://github.com/nikescar/libsqlite3-hotbundle.git?rev=a208c7ef1447a02bf5ecb8c6a297aca012d41244#a208c7ef1447a02bf5ecb8c6a297aca012d41244" -dependencies = [ - "cc", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 17cafe09c5..ba0401009c 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1661,7 +1661,7 @@ async function openDbFilePath(path: string) { password: "", }; await connectionStore.addConnection(config); - await connectionStore.connect(config); + void connectionStore.connect(config); toast(t("welcome.fileOpened", { name })); } catch (e: any) { toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000); diff --git a/apps/desktop/src/composables/useFileDrop.ts b/apps/desktop/src/composables/useFileDrop.ts index 4b284b02ad..cb4b7ff0a1 100644 --- a/apps/desktop/src/composables/useFileDrop.ts +++ b/apps/desktop/src/composables/useFileDrop.ts @@ -109,10 +109,10 @@ export function useFileDrop() { }; try { await connectionStore.addConnection(config); - await connectionStore.connect(config); + void connectionStore.connect(config); toast(t("welcome.fileOpened", { name })); } catch (e: any) { - toast(t("welcome.fileOpenFailed", { name, message: e?.message || String(e) }), 5000); + toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000); } } }); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fb1e95b054..04ff8f6533 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -436,7 +436,7 @@ export default { createDuckDbFile: "Create DuckDB file", createSqliteFile: "Create SQLite database", memoryDatabasePathHint: "Use :memory: to create an in-memory SQLite or DuckDB database.", - sqliteCipherKey: "SQLite Encryption Key", + sqliteCipherKey: "SQLCipher Key", sqliteCipherKeyPlaceholder: "Leave empty for unencrypted SQLite", sqliteExtensions: "SQLite Extensions", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 638b2bc8c5..b65090a27c 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -438,7 +438,7 @@ export default withEnglishFallback({ createDuckDbFile: "Crear archivo DuckDB", createSqliteFile: "Crear base de datos SQLite", memoryDatabasePathHint: "Usa :memory: para crear una base de datos SQLite o DuckDB en memoria.", - sqliteCipherKey: "Clave de cifrado de SQLite", + sqliteCipherKey: "Clave SQLCipher", sqliteCipherKeyPlaceholder: "Déjalo vacío para SQLite sin cifrar", sqliteExtensions: "Extensiones de SQLite", sqliteExtensionsPlaceholder: "/ruta/a/regexp.dylib\n/ruta/a/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 3e5c0d05aa..522b5de47f 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -437,7 +437,7 @@ export default withEnglishFallback({ createDuckDbFile: "Crea file DuckDB", createSqliteFile: "Crea database SQLite", memoryDatabasePathHint: "Usa :memory: per creare un database SQLite o DuckDB in memoria.", - sqliteCipherKey: "Chiave di crittografia SQLite", + sqliteCipherKey: "Chiave SQLCipher", sqliteCipherKeyPlaceholder: "Lascia vuoto per SQLite non cifrato", sqliteExtensions: "Estensioni SQLite", sqliteExtensionsPlaceholder: "/percorso/per/regexp.dylib\n/percorso/per/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 2f2b4f6d04..e9668603ab 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -437,7 +437,7 @@ export default withEnglishFallback({ createDuckDbFile: "DuckDBファイルを作成", createSqliteFile: "SQLiteデータベースを作成", memoryDatabasePathHint: "インメモリのSQLiteまたはDuckDBデータベースを作成するには :memory: を使用してください。", - sqliteCipherKey: "SQLite暗号化キー", + sqliteCipherKey: "SQLCipherキー", sqliteCipherKeyPlaceholder: "暗号化されていないSQLiteでは空のままにします", sqliteExtensions: "SQLite拡張", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index b9db45c821..2a9b5b65d9 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -430,7 +430,7 @@ export default withEnglishFallback({ createDuckDbFile: "DuckDB 파일 만들기", createSqliteFile: "SQLite 데이터베이스 만들기", memoryDatabasePathHint: "인메모리 SQLite 또는 DuckDB 데이터베이스를 만들려면 :memory:를 사용하세요.", - sqliteCipherKey: "SQLite 암호화 키", + sqliteCipherKey: "SQLCipher 키", sqliteCipherKeyPlaceholder: "암호화하지 않은 SQLite는 비워 두세요", sqliteExtensions: "SQLite 확장", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 53194ab37d..806011ce06 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -438,7 +438,7 @@ export default withEnglishFallback({ createDuckDbFile: "Criar arquivo DuckDB", createSqliteFile: "Criar banco de dados SQLite", memoryDatabasePathHint: "Use :memory: para criar um banco de dados SQLite ou DuckDB na memória.", - sqliteCipherKey: "Chave de criptografia do SQLite", + sqliteCipherKey: "Chave SQLCipher", sqliteCipherKeyPlaceholder: "Deixe vazio para SQLite não criptografado", sqliteExtensions: "Extensões SQLite", sqliteExtensionsPlaceholder: "/caminho/para/regexp.dylib\n/caminho/para/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 7b8c58efb3..4814958f00 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -361,7 +361,7 @@ export default withEnglishFallback({ createDuckDbFile: "新建 DuckDB 文件", createSqliteFile: "新建 SQLite 数据库", memoryDatabasePathHint: "输入 :memory: 可创建 SQLite 或 DuckDB 内存数据库。", - sqliteCipherKey: "SQLite 加密密码", + sqliteCipherKey: "SQLCipher 密码(密钥)", sqliteCipherKeyPlaceholder: "未加密 SQLite 留空", sqliteExtensions: "SQLite 扩展库", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index b430fca737..ac9e885a90 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -438,7 +438,7 @@ export default withEnglishFallback({ createDuckDbFile: "建立 DuckDB 檔案", createSqliteFile: "建立 SQLite 資料庫", memoryDatabasePathHint: "輸入 :memory: 可建立 SQLite 或 DuckDB 記憶體資料庫。", - sqliteCipherKey: "SQLite 加密密碼", + sqliteCipherKey: "SQLCipher 密碼(密鑰)", sqliteCipherKeyPlaceholder: "未加密 SQLite 留空", sqliteExtensions: "SQLite 擴充庫", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", diff --git a/apps/desktop/src/lib/connection/__tests__/connectionErrorHints.spec.ts b/apps/desktop/src/lib/connection/__tests__/connectionErrorHints.spec.ts index 00a9fe628d..28d3d19d93 100644 --- a/apps/desktop/src/lib/connection/__tests__/connectionErrorHints.spec.ts +++ b/apps/desktop/src/lib/connection/__tests__/connectionErrorHints.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { appendConnectionErrorHints, isSqliteMissingEncryptionPasswordFailure } from "@/lib/connection/connectionErrorHints"; +import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints"; import type { ConnectionConfig } from "@/types/database"; function mysqlConfig(urlParams: string | undefined): ConnectionConfig { @@ -40,14 +40,6 @@ const t = (key: string) => { }; describe("appendConnectionErrorHints", () => { - it("recognizes an encrypted SQLite file that was first opened without a password", () => { - const config = { ...mysqlConfig(undefined), db_type: "sqlite" as const, host: "/tmp/encrypted.db", port: 0 }; - - expect(isSqliteMissingEncryptionPasswordFailure(config, "Selected file is not a valid SQLite database file.")).toBe(true); - expect(isSqliteMissingEncryptionPasswordFailure({ ...config, password: "wrong" }, "Selected file is not a valid SQLite database file.")).toBe(false); - expect(isSqliteMissingEncryptionPasswordFailure(config, "SQLite connection failed: unable to open database file")).toBe(false); - }); - it("adds a MySQL TLS hint for non-disabled TLS failures", () => { const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "MySQL connection failed: TLS handshake failed", t); diff --git a/apps/desktop/src/lib/connection/connectionErrorHints.ts b/apps/desktop/src/lib/connection/connectionErrorHints.ts index 80428552ef..2a546235b9 100644 --- a/apps/desktop/src/lib/connection/connectionErrorHints.ts +++ b/apps/desktop/src/lib/connection/connectionErrorHints.ts @@ -72,11 +72,6 @@ export function isMysqlMissingPasswordFailure(config: ConnectionConfig, message: return /access denied for user[\s\S]*using password:\s*no/i.test(message); } -export function isSqliteMissingEncryptionPasswordFailure(config: ConnectionConfig, message: string): boolean { - if (config.db_type !== "sqlite" || config.password) return false; - return /Selected file is not a valid SQLite database file/i.test(message); -} - export function isJdbcMissingRuntimeDependencyError(message: string): boolean { return /Missing Java class|NoClassDefFoundError|ClassNotFoundException/i.test(message); } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts index e88a730a87..de8677d839 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts @@ -297,35 +297,6 @@ describe("connectionStore save_password opt-out", () => { expect(store.getConfig("mysql-1")?.password).toBe("typed-pw"); }); - it("prompts and retries when an encrypted SQLite file is opened without a password", async () => { - const connectDb = vi.fn().mockRejectedValueOnce(new Error("Selected file is not a valid SQLite database file.")).mockResolvedValueOnce("sqlite-1"); - installApiMocks({ connectDb }); - installPasswordPromptMock(); - requestPassword.mockResolvedValue({ password: "123456", rememberPassword: true }); - const { useConnectionStore } = await import("@/stores/connectionStore"); - const store = useConnectionStore(); - const connection = postgresConnection({ - id: "sqlite-1", - name: "Encrypted SQLite", - db_type: "sqlite", - host: "/tmp/encrypted.db", - port: 0, - username: "", - database: undefined, - save_password: true, - password: "", - }); - store.connections = [connection]; - - await store.connect(connection); - - expect(requestPassword).toHaveBeenCalledWith({ connectionId: "sqlite-1", connectionName: "Encrypted SQLite" }); - expect(connectDb).toHaveBeenCalledTimes(2); - expect(connectDb).toHaveBeenNthCalledWith(1, expect.objectContaining({ password: "" }), expect.any(Number)); - expect(connectDb).toHaveBeenNthCalledWith(2, expect.objectContaining({ password: "123456" }), expect.any(Number)); - expect(store.getConfig("sqlite-1")?.password).toBe("123456"); - }); - it("does not persist a recovered password after the connection config changes", async () => { const connected = deferred(); const connectDb = vi diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 011eafa6b7..6c727b1c83 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -141,7 +141,7 @@ import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern"; import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias"; import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates"; import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint"; -import { appendConnectionErrorHints, isMysqlMissingPasswordFailure, isSqliteMissingEncryptionPasswordFailure } from "@/lib/connection/connectionErrorHints"; +import { appendConnectionErrorHints, isMysqlMissingPasswordFailure } from "@/lib/connection/connectionErrorHints"; import { connectionNeedsPasswordPrompt } from "@/lib/connection/connectionPassword"; import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases"; import { buildXuguTypeMemberNodes, isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers"; @@ -3630,13 +3630,12 @@ export const useConnectionStore = defineStore("connection", () => { rebuildTreeNodes(); } - async function connectDbWithPasswordRetry(config: ConnectionConfig, localAttempt: number): Promise<{ config: ConnectionConfig; id: string; rememberPassword: boolean }> { + async function connectDbWithMissingPasswordRetry(config: ConnectionConfig, localAttempt: number): Promise<{ config: ConnectionConfig; id: string; rememberPassword: boolean }> { try { const id = await withConnectionAttemptTimeout(api.connectDb(config, localAttempt), config); return { config, id, rememberPassword: false }; } catch (error) { - const message = connectionErrorMessage(error); - if (!isMysqlMissingPasswordFailure(config, message) && !isSqliteMissingEncryptionPasswordFailure(config, message)) throw error; + if (!isMysqlMissingPasswordFailure(config, connectionErrorMessage(error))) throw error; const prompted = await ensureConnectionPassword(config, true); config = prompted.config; ensureLocalConnectionAttemptActive(config.id, localAttempt); @@ -3662,7 +3661,7 @@ export const useConnectionStore = defineStore("connection", () => { await ensureSqlServerLegacyCompatibilityComponentInstalled(config); } ensureLocalConnectionAttemptActive(config.id, localAttempt); - const connection = await connectDbWithPasswordRetry(config, localAttempt); + const connection = await connectDbWithMissingPasswordRetry(config, localAttempt); config = connection.config; rememberPassword ||= connection.rememberPassword; const id = connection.id; @@ -3905,7 +3904,7 @@ export const useConnectionStore = defineStore("connection", () => { await ensureSqlServerLegacyCompatibilityComponentInstalled(config); } ensureLocalConnectionAttemptActive(connectionId, localAttempt); - const connection = await connectDbWithPasswordRetry(config, localAttempt); + const connection = await connectDbWithMissingPasswordRetry(config, localAttempt); config = connection.config; rememberPassword ||= connection.rememberPassword; const id = connection.id; diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index ec4e350875..f40dee74ca 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -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"] } diff --git a/crates/dbx-core/src/db/sqlite.rs b/crates/dbx-core/src/db/sqlite.rs index c5ca15b24c..e1f11d8b04 100644 --- a/crates/dbx-core/src/db/sqlite.rs +++ b/crates/dbx-core/src/db/sqlite.rs @@ -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 { 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] = if encrypted { &[None, Some(3), Some(2), Some(1)] } else { &[None] }; let mut unlock_error: Option = 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 { @@ -217,61 +194,48 @@ fn sqlite_cipher_key(cipher_key: &str) -> Option { } #[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) -> 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, ) -> 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] diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 8d2cf2bb24..eba281b3b0 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -380,7 +380,7 @@ mod tests { Ok(_) => panic!("SQLCipher attachments must be rejected"), Err(error) => error, }; - assert!(error.contains("encrypted connections"), "{error}"); + assert!(error.contains("SQLCipher"), "{error}"); } #[tokio::test] @@ -600,7 +600,7 @@ mod tests { Ok(_) => panic!("wrong SQLCipher key must fail"), Err(err) => err, }; - assert!(wrong_key.contains("Encrypted SQLite database unlock failed")); + assert!(wrong_key.contains("SQLCipher database unlock failed")); let missing_key = match connect_sqlite_from_config(&sqlite_config(&path, "")).await { Ok(_) => panic!("missing SQLCipher key must fail"),