From 87983d306a251893c22607174649c34fa97c03be Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:10 +0800 Subject: [PATCH 1/6] feat: add SQLCipher variant to DatabaseType and route through SQLite adapter SQLCipher is an encrypted SQLite variant. It reuses the existing SQLite adapter (CoreDatabaseType::SQLite) with an encryption key applied via PRAGMA key on connection. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src-tauri/src/database/config.rs | 2 ++ src-tauri/src/database/strategy.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/database/config.rs b/src-tauri/src/database/config.rs index c0b7c16..a38def1 100644 --- a/src-tauri/src/database/config.rs +++ b/src-tauri/src/database/config.rs @@ -24,6 +24,8 @@ pub enum DatabaseType { SqlServer, /// SQLite. SQLite, + /// SQLCipher — encrypted SQLite variant (same adapter, requires encryption key). + SQLCipher, /// DuckDB — JDBC bridge. DuckDb, /// ClickHouse (HTTP protocol). diff --git a/src-tauri/src/database/strategy.rs b/src-tauri/src/database/strategy.rs index ea7c28f..86ffa02 100644 --- a/src-tauri/src/database/strategy.rs +++ b/src-tauri/src/database/strategy.rs @@ -59,7 +59,7 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy { // Other native adapters SqlServer => ConnectionStrategy::Native(CoreDatabaseType::SqlServer), - SQLite => ConnectionStrategy::Native(CoreDatabaseType::SQLite), + SQLite | SQLCipher => ConnectionStrategy::Native(CoreDatabaseType::SQLite), ClickHouse => ConnectionStrategy::Native(CoreDatabaseType::ClickHouse), // JDBC bridge (Java subprocess) @@ -131,7 +131,7 @@ pub fn default_port(db: DatabaseType) -> Option { Databend => Some(3307), ManticoreSearch => Some(9306), SqlServer => Some(1433), - SQLite => None, + SQLite | SQLCipher => None, DuckDb => None, ClickHouse => Some(8123), Firebird => Some(3050), From 3ff84704c2a29440189e04b9c65a26a4e1741550 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:16 +0800 Subject: [PATCH 2/6] feat: add encryption key support to SQLite adapter for SQLCipher connections SQLitePool and SQLiteAdapter now accept an optional encryption_key. When set (SQLCipher), the key is applied via PRAGMA key after opening the connection. Without the sqlcipher cargo feature, the PRAGMA is silently ignored, preserving backward compatibility with regular SQLite. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src-tauri/src/database/sqlite.rs | 41 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/database/sqlite.rs b/src-tauri/src/database/sqlite.rs index a4668e8..f19f1f8 100644 --- a/src-tauri/src/database/sqlite.rs +++ b/src-tauri/src/database/sqlite.rs @@ -47,19 +47,23 @@ const MEMORY_DB: &str = ":memory:"; /// /// SQLite has limited concurrency support compared to client-server databases. /// This pool manages connections with proper synchronization for thread-safety. +/// For SQLCipher, an encryption key is stored and applied via `PRAGMA key` on +/// each newly created connection. pub struct SQLitePool { connections: Arc>>>>, max_connections: usize, db_path: Option, + encryption_key: Option, } impl SQLitePool { /// Create a new SQLite connection pool. - fn new(max_connections: usize, db_path: Option) -> Self { + fn new(max_connections: usize, db_path: Option, encryption_key: Option) -> Self { Self { connections: Arc::new(Mutex::new(Vec::new())), max_connections, db_path, + encryption_key, } } @@ -110,8 +114,22 @@ impl SQLitePool { })? }; + // Apply SQLCipher encryption key if provided + // + // SQLCipher uses `PRAGMA key` to set the passphrase. When compiled with + // the `sqlcipher` cargo feature, rusqlite links against SQLCipher instead + // of SQLite, and this pragma activates encryption. Without the feature + // (default SQLite build), the pragma is silently ignored. + if let Some(ref key) = self.encryption_key { + conn.execute(&format!("PRAGMA key = '{}'", key.replace('\'', "''")), []) + .map_err(|e| { + DbError::Configuration(format!("Failed to set SQLCipher encryption key: {}", e)) + })?; + } + // Enable Write-Ahead Logging (WAL) mode for better concurrency // WAL mode allows multiple readers and one writer to proceed concurrently + // Note: SQLCipher with WAL requires the same key to be set on each connection. if self.db_path.is_some() { conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())) .map_err(|e| DbError::Configuration(format!("Failed to enable WAL mode: {}", e)))?; @@ -201,12 +219,14 @@ impl ConnectionPool for SQLitePool { /// SQLite database adapter. /// -/// Supports both file-based and in-memory SQLite databases with proper -/// thread-safety through connection pooling and Write-Ahead Logging (WAL) mode. +/// Supports both file-based and in-memory SQLite databases (and SQLCipher +/// encrypted databases) with proper thread-safety through connection pooling +/// and Write-Ahead Logging (WAL) mode. pub struct SQLiteAdapter { config: ConnectionConfig, pool: Option>, db_path: Option, + encryption_key: Option, } impl SQLiteAdapter { @@ -215,6 +235,8 @@ impl SQLiteAdapter { /// The database path can be specified in the config.database field: /// - For file-based databases: Use a file path (e.g., "/path/to/db.sqlite") /// - For in-memory databases: Use ":memory:" or leave database as None + /// + /// For SQLCipher, the encryption passphrase can be provided via `config.password`. pub fn new(config: ConnectionConfig) -> Self { let db_path = config.database.as_ref().and_then(|db| { if db == MEMORY_DB { @@ -224,10 +246,17 @@ impl SQLiteAdapter { } }); + let encryption_key = if config.db_type == crate::database::config::DatabaseType::SQLCipher { + config.password.clone() + } else { + None + }; + Self { config, pool: None, db_path, + encryption_key, } } @@ -422,7 +451,11 @@ impl DatabaseAdapter for SQLiteAdapter { self.config.pool_config.max_connections as usize }; - let pool = SQLitePool::new(max_connections, self.db_path.clone()); + let pool = SQLitePool::new( + max_connections, + self.db_path.clone(), + self.encryption_key.clone(), + ); // Test the connection by creating one let conn = pool.get_conn().await?; From a26df4029e4e63f53daa810d9d038441d5041b7d Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:23 +0800 Subject: [PATCH 3/6] feat: wire SQLCipher through backend state, commands, and transfer modules Adds SQLCipher parsing in state.rs (parse_db_type, to_connection_config), helpers.rs (db_type_to_enum, is_file_based_db), and maps SQLCipher to SQLite DDL generation and type conversion in the transfer module. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src-tauri/src/commands/helpers.rs | 5 ++++- src-tauri/src/state.rs | 5 +++-- src-tauri/src/transfer/ddl.rs | 4 +++- src-tauri/src/transfer/migration.rs | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands/helpers.rs b/src-tauri/src/commands/helpers.rs index 85de96c..56ddca3 100644 --- a/src-tauri/src/commands/helpers.rs +++ b/src-tauri/src/commands/helpers.rs @@ -149,6 +149,7 @@ fn db_type_to_enum(db_type: &str) -> Result Ok(DatabaseType::MySQL), "sqlserver" | "mssql" => Ok(DatabaseType::SqlServer), "sqlite" => Ok(DatabaseType::SQLite), + "sqlcipher" => Ok(DatabaseType::SQLCipher), "duckdb" | "duck_db" | "duck" => Ok(DatabaseType::DuckDb), "clickhouse" => Ok(DatabaseType::ClickHouse), "firebird" => Ok(DatabaseType::Firebird), @@ -218,7 +219,9 @@ fn db_type_to_enum(db_type: &str) -> Result bool { matches!( db_type, - crate::database::DatabaseType::SQLite | crate::database::DatabaseType::DuckDb + crate::database::DatabaseType::SQLite + | crate::database::DatabaseType::SQLCipher + | crate::database::DatabaseType::DuckDb ) } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 8286a93..590660f 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -132,6 +132,7 @@ impl ServerConfig { "mysql" => Ok(DatabaseType::MySQL), "sqlserver" | "mssql" => Ok(DatabaseType::SqlServer), "sqlite" => Ok(DatabaseType::SQLite), + "sqlcipher" => Ok(DatabaseType::SQLCipher), "duckdb" | "duck" => Ok(DatabaseType::DuckDb), "clickhouse" => Ok(DatabaseType::ClickHouse), "firebird" => Ok(DatabaseType::Firebird), @@ -209,8 +210,8 @@ impl ServerConfig { } let db_lower = self.db_type.to_lowercase(); - // DuckDB is file-based, uses JDBC bridge with jdbc:duckdb:{filepath} - if db_lower == "sqlite" || db_lower == "duckdb" || db_lower == "duck" { + // SQLite, SQLCipher, and DuckDB are file-based + if db_lower == "sqlite" || db_lower == "sqlcipher" || db_lower == "duckdb" || db_lower == "duck" { config = config.with_database(&self.host); } else if let Some(ref database) = self.database { config = config.with_database(database); diff --git a/src-tauri/src/transfer/ddl.rs b/src-tauri/src/transfer/ddl.rs index 303a042..d27d3af 100644 --- a/src-tauri/src/transfer/ddl.rs +++ b/src-tauri/src/transfer/ddl.rs @@ -15,7 +15,9 @@ pub fn generate_ddl_for_engine( match engine { DatabaseType::PostgreSQL => generate_postgres_ddl(schema, table, columns, options), DatabaseType::MySQL => generate_mysql_ddl(schema, table, columns, options), - DatabaseType::SQLite => generate_sqlite_ddl(schema, table, columns, options), + DatabaseType::SQLite | DatabaseType::SQLCipher => { + generate_sqlite_ddl(schema, table, columns, options) + } DatabaseType::SqlServer => generate_sqlserver_ddl(schema, table, columns, options), _ => generate_generic_ddl(schema, table, columns, options), } diff --git a/src-tauri/src/transfer/migration.rs b/src-tauri/src/transfer/migration.rs index 9545ae2..748bb77 100644 --- a/src-tauri/src/transfer/migration.rs +++ b/src-tauri/src/transfer/migration.rs @@ -631,7 +631,7 @@ fn map_type_to_engine(source_type: &str, target_engine: DatabaseType) -> String "bytea" => "BLOB".to_string(), _ => source_type.to_string(), }, - DatabaseType::SQLite => match source_lower.as_str() { + DatabaseType::SQLite | DatabaseType::SQLCipher => match source_lower.as_str() { "int" | "integer" | "bigint" | "smallint" | "tinyint" => "INTEGER".to_string(), "varchar" | "char" | "text" => "TEXT".to_string(), "datetime" | "timestamp" | "date" => "TEXT".to_string(), From 7ccebf34312d7ed714bc61e4852c31f769907d71 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:30 +0800 Subject: [PATCH 4/6] build: add sqlcipher cargo feature for rusqlite/bundled-sqlcipher Adds an optional sqlcipher cargo feature that compiles rusqlite with bundled-sqlcipher instead of bundled SQLite. SQLCipher is backwards-compatible with regular SQLite databases. Build with: cargo build --no-default-features --features sqlcipher Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c511055..daf5af1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6433,7 +6433,7 @@ dependencies = [ [[package]] name = "sqlkit" -version = "0.7.8" +version = "0.7.9" dependencies = [ "async-trait", "base64 0.22.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 353dbe8..09f403b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -58,10 +58,10 @@ tiberius = { version = "0.12", default-features = false, features = [ "rust_decimal" ] } tokio-util = { version = "0.7", features = [ "compat" ] } -rusqlite = { version = "0.32", features = [ - "bundled", - "chrono" -] } +rusqlite = { version = "0.32", default-features = false, features = [ "chrono" ] } +# C library bundling is selected by cargo features: +# default → rusqlite/bundled (SQLite) +# sqlcipher → rusqlite/bundled-sqlcipher (SQLCipher, encrypted SQLite) uuid = { version = "1", features = [ "v4" ] } tauri-plugin-store = "2" tauri-plugin-dialog = "2" @@ -109,5 +109,8 @@ mockall = "0.13" tempfile = "3" [features] -default = [] +default = [ "rusqlite/bundled" ] ohos = [] +# Use SQLCipher (encrypted SQLite) instead of plain SQLite. +# Build with: cargo build --no-default-features --features sqlcipher +sqlcipher = [ "rusqlite/bundled-sqlcipher" ] From 33ca53a71ba9f0dea71daac627071159a4849adc Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:37 +0800 Subject: [PATCH 5/6] feat: add SQLCipher frontend types, icon, and i18n labels Adds SQLCIPHER enum to DatabaseType, backend mappings (dbTypeToBackend/dbTypeFromBackend), SVG logo (green-tinted SQLite icon with lock motif), and i18n labels for enUS and zhCN. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/assets/images/database-icons/sqlcipher-logo.svg | 1 + src/composables/useDatabaseIcon.ts | 2 ++ src/lang/enUS.ts | 3 +++ src/lang/zhCN.ts | 2 ++ src/store/connectionStore.ts | 3 +++ 5 files changed, 11 insertions(+) create mode 100644 src/assets/images/database-icons/sqlcipher-logo.svg diff --git a/src/assets/images/database-icons/sqlcipher-logo.svg b/src/assets/images/database-icons/sqlcipher-logo.svg new file mode 100644 index 0000000..559bd42 --- /dev/null +++ b/src/assets/images/database-icons/sqlcipher-logo.svg @@ -0,0 +1 @@ + diff --git a/src/composables/useDatabaseIcon.ts b/src/composables/useDatabaseIcon.ts index 20ab139..52d33a9 100644 --- a/src/composables/useDatabaseIcon.ts +++ b/src/composables/useDatabaseIcon.ts @@ -46,6 +46,7 @@ import rqliteLogo from '@/assets/images/database-icons/rqlite-logo.svg' import selectdbLogo from '@/assets/images/database-icons/selectdb-logo.svg' import singlestoreLogo from '@/assets/images/database-icons/singlestorememsql-logo.svg' import snowflakeLogo from '@/assets/images/database-icons/snowflake-logo.svg' +import sqlcipherLogo from '@/assets/images/database-icons/sqlcipher-logo.svg' import sqliteLogo from '@/assets/images/database-icons/sqlite-logo.svg' import sqlserverLogo from '@/assets/images/database-icons/sqlserver-logo.svg' import starrocksLogo from '@/assets/images/database-icons/starrocks-logo.svg' @@ -73,6 +74,7 @@ const databaseIcons: Record = { MYSQL: { icon: mysqlLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, MARIADB: { icon: mariadbLogo, color: 'bg-purple-100 dark:bg-purple-900/30' }, SQLITE: { icon: sqliteLogo, color: 'bg-green-100 dark:bg-green-900/30' }, + SQLCIPHER: { icon: sqlcipherLogo, color: 'bg-green-100 dark:bg-green-900/30' }, SQLSERVER: { icon: sqlserverLogo, color: 'bg-red-100 dark:bg-red-900/30' }, DUCKDB: { icon: duckdbLogo, color: 'bg-yellow-100 dark:bg-yellow-900/30' }, CLICKHOUSE: { icon: clickhouseLogo, color: 'bg-red-100 dark:bg-red-900/30' }, diff --git a/src/lang/enUS.ts b/src/lang/enUS.ts index 9a82ef9..bd8d2f6 100644 --- a/src/lang/enUS.ts +++ b/src/lang/enUS.ts @@ -913,6 +913,7 @@ export const enUS = { databaseType: 'Database Type', host: 'Host', databaseFilePath: 'Database File Path', + encryptionKey: 'Encryption Key', port: 'Port', database: 'Database', username: 'Username', @@ -926,6 +927,7 @@ export const enUS = { selectType: 'Select database type', host: 'localhost', filePath: '/path/to/database.db', + encryptionKey: 'Enter encryption passphrase', database: 'database_name', username: 'username', password: '••••••••', @@ -936,6 +938,7 @@ export const enUS = { 'mysql': 'MySQL', 'mariadb': 'MariaDB', 'sqlite': 'SQLite', + 'sqlcipher': 'SQLCipher', 'duckdb': 'DuckDB', 'clickhouse': 'ClickHouse', 'cockroachdb': 'CockroachDB', diff --git a/src/lang/zhCN.ts b/src/lang/zhCN.ts index e2a9bc3..88f34fe 100644 --- a/src/lang/zhCN.ts +++ b/src/lang/zhCN.ts @@ -913,6 +913,7 @@ export const zhCN = { databaseType: '数据库类型', host: '主机地址', databaseFilePath: '数据库文件路径', + encryptionKey: '加密密钥', port: '端口', database: '数据库', username: '用户名', @@ -936,6 +937,7 @@ export const zhCN = { 'mysql': 'MySQL', 'mariadb': 'MariaDB', 'sqlite': 'SQLite', + 'sqlcipher': 'SQLCipher', 'duckdb': 'DuckDB', 'clickhouse': 'ClickHouse', 'cockroachdb': 'CockroachDB', diff --git a/src/store/connectionStore.ts b/src/store/connectionStore.ts index 9ba0894..12f312a 100644 --- a/src/store/connectionStore.ts +++ b/src/store/connectionStore.ts @@ -9,6 +9,7 @@ export enum DatabaseType { POSTGRESQL = 'POSTGRESQL', MARIADB = 'MARIADB', SQLITE = 'SQLITE', + SQLCIPHER = 'SQLCIPHER', SQLSERVER = 'SQLSERVER', DUCKDB = 'DUCKDB', CLICKHOUSE = 'CLICKHOUSE', @@ -81,6 +82,7 @@ export const dbTypeToBackend: Record = { [DatabaseType.MYSQL]: MYSQL_BACKEND, [DatabaseType.MARIADB]: 'mariadb', [DatabaseType.SQLITE]: 'SQLite', + [DatabaseType.SQLCIPHER]: 'sqlcipher', [DatabaseType.SQLSERVER]: 'SqlServer', [DatabaseType.DUCKDB]: 'duckdb', [DatabaseType.CLICKHOUSE]: 'clickhouse', @@ -151,6 +153,7 @@ export const dbTypeFromBackend: Record = { 'mariadb': DatabaseType.MARIADB, 'SqlServer': DatabaseType.SQLSERVER, 'SQLite': DatabaseType.SQLITE, + 'sqlcipher': DatabaseType.SQLCIPHER, 'duckdb': DatabaseType.DUCKDB, 'clickhouse': DatabaseType.CLICKHOUSE, 'cockroachdb': DatabaseType.COCKROACHDB, From fca99d0f52d76c079be9e3032abdd22c7f4fdf5d Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 27 Jul 2026 19:49:43 +0800 Subject: [PATCH 6/6] feat: add SQLCipher connection form with file picker and encryption key field SQLCipher shows a file picker for the database path (like SQLite) plus a password field for the encryption key. Port defaults to 0. Uses the existing file-based form patterns. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../connections/ServerFormDialog.vue | 249 +++++++++++++----- 1 file changed, 176 insertions(+), 73 deletions(-) diff --git a/src/components/connections/ServerFormDialog.vue b/src/components/connections/ServerFormDialog.vue index f7011a9..cf6874e 100644 --- a/src/components/connections/ServerFormDialog.vue +++ b/src/components/connections/ServerFormDialog.vue @@ -52,6 +52,7 @@ const defaultPorts: Record = { [DatabaseType.MYSQL]: 3306, [DatabaseType.MARIADB]: 3306, [DatabaseType.SQLITE]: 0, + [DatabaseType.SQLCIPHER]: 0, [DatabaseType.SQLSERVER]: 1433, [DatabaseType.DUCKDB]: 0, [DatabaseType.CLICKHOUSE]: 8123, @@ -314,21 +315,29 @@ function handleDatabaseTypeChange(value: string) { else { formData.value.oracleOptions = undefined } - if (type === DatabaseType.SQLITE || type === DatabaseType.DUCKDB) { + if (type === DatabaseType.SQLITE || type === DatabaseType.SQLCIPHER || type === DatabaseType.DUCKDB) { formData.value.host = '' formData.value.port = 0 - formData.value.username = '' - formData.value.password = '' - sqliteTab.value = 'file' + if (type === DatabaseType.SQLITE || type === DatabaseType.DUCKDB) { + formData.value.username = '' + formData.value.password = '' + } + sqliteTab.value = type === DatabaseType.SQLITE ? 'file' : 'file' savedFilePath.value = '' } } const isFileBased = computed(() => formData.value.type === DatabaseType.SQLITE + || formData.value.type === DatabaseType.SQLCIPHER || formData.value.type === DatabaseType.DUCKDB, ) +// SQLCipher is file-based but also needs a password (encryption key) +const isEncryptedFileDb = computed(() => + formData.value.type === DatabaseType.SQLCIPHER, +) + const isJdbcDb = computed(() => isJdbcDatabase(formData.value.type)) const dbTypeI18nKeyMap: Record = { @@ -507,7 +516,7 @@ async function selectDatabaseFile() { const selected = await openDialog({ multiple: false, filters: [ - { name: 'SQLite', extensions: ['db', 'sqlite', 'sqlite3'] }, + { name: 'Database', extensions: ['db', 'sqlite', 'sqlite3', 'sqlcipher', 'db3'] }, { name: 'DuckDB', extensions: ['duckdb', 'db'] }, { name: 'All Files', extensions: ['*'] }, ], @@ -762,8 +771,11 @@ function handleSave() { return } - // Save recent database path for SQLite - if (formData.value.type === DatabaseType.SQLITE && formData.value.host !== ':memory:') { + // Save recent database path for file-based databases + if ( + (formData.value.type === DatabaseType.SQLITE || formData.value.type === DatabaseType.SQLCIPHER) + && formData.value.host !== ':memory:' + ) { saveRecentDatabase(formData.value.host) } @@ -1102,83 +1114,174 @@ function handleSave() { - +
- - - - - {{ t('components.serverForm.sqlite.modes.file') }} - - - {{ t('components.serverForm.sqlite.modes.inMemory') }} - - - - - - -
- -
- - + + + + + - - -

- {{ t('components.serverForm.sqlite.inMemoryHint') }} + +

- -
+ +
+ +
+ + +
+
+