diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73910c13f..335ca9ae0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,7 @@ jobs: -p cdk --no-default-features --features "mint swagger", -p cdk-redb, -p cdk-sqlite, + -p cdk-sqlite --features sqlcipher, -p cdk-axum --no-default-features, -p cdk-axum --no-default-features --features swagger, -p cdk-axum --no-default-features --features redis, @@ -104,10 +105,12 @@ jobs: -p cdk-lnbits, -p cdk-fake-wallet, --bin cdk-cli, + --bin cdk-cli --features sqlcipher, --bin cdk-mintd, --bin cdk-mintd --features redis, --bin cdk-mintd --features redb, --bin cdk-mintd --features "redis swagger redb", + --bin cdk-mintd --features sqlcipher, --bin cdk-mintd --no-default-features --features lnd, --bin cdk-mintd --no-default-features --features cln, --bin cdk-mintd --no-default-features --features lnbits, diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index 4e258d455..aaf814f73 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true edition.workspace = true rust-version.workspace = true +[features] +sqlcipher = ["cdk-sqlite/sqlcipher"] + [dependencies] anyhow.workspace = true bip39.workspace = true diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7336d1095..bad6212e3 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -31,6 +31,10 @@ struct Cli { /// Database engine to use (sqlite/redb) #[arg(short, long, default_value = "sqlite")] engine: String, + /// Database password for sqlcipher + #[cfg(feature = "sqlcipher")] + #[arg(long)] + password: Option, /// Path to working dir #[arg(short, long)] work_dir: Option, @@ -106,7 +110,15 @@ async fn main() -> Result<()> { match args.engine.as_str() { "sqlite" => { let sql_path = work_dir.join("cdk-cli.sqlite"); + #[cfg(not(feature = "sqlcipher"))] let sql = WalletSqliteDatabase::new(&sql_path).await?; + #[cfg(feature = "sqlcipher")] + let sql = { + match args.password { + Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?, + None => bail!("Missing database password"), + } + }; sql.migrate().await; diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index b4bd2d5ea..553939720 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -17,6 +17,7 @@ redis = ["cdk-axum/redis"] management-rpc = ["cdk-mint-rpc"] # MSRV is not commited to with redb enabled redb = ["dep:cdk-redb"] +sqlcipher = ["cdk-sqlite/sqlcipher"] cln = ["dep:cdk-cln"] lnd = ["dep:cdk-lnd"] lnbits = ["dep:cdk-lnbits"] diff --git a/crates/cdk-mintd/src/cli.rs b/crates/cdk-mintd/src/cli.rs index 60789c231..20d07086f 100644 --- a/crates/cdk-mintd/src/cli.rs +++ b/crates/cdk-mintd/src/cli.rs @@ -12,6 +12,9 @@ pub struct CLIArgs { required = false )] pub work_dir: Option, + #[cfg(feature = "sqlcipher")] + #[arg(short, long, help = "Database password for sqlcipher", required = true)] + pub password: String, #[arg( short, long, diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 7fb9ad3e6..1dc18e0dd 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -111,7 +111,10 @@ async fn main() -> anyhow::Result<()> { match settings.database.engine { DatabaseEngine::Sqlite => { let sql_db_path = work_dir.join("cdk-mintd.sqlite"); + #[cfg(not(feature = "sqlcipher"))] let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; + #[cfg(feature = "sqlcipher")] + let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?; sqlite_db.migrate().await; diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml index f1577973c..369623c12 100644 --- a/crates/cdk-sqlite/Cargo.toml +++ b/crates/cdk-sqlite/Cargo.toml @@ -14,6 +14,7 @@ rust-version = "1.75.0" # MSRV default = ["mint", "wallet"] mint = ["cdk-common/mint"] wallet = ["cdk-common/wallet"] +sqlcipher = ["libsqlite3-sys"] [dependencies] async-trait.workspace = true @@ -26,6 +27,7 @@ sqlx = { version = "0.6.3", default-features = false, features = [ "migrate", "uuid", ] } +libsqlite3-sys = { version = "0.24.1", features = ["bundled-sqlcipher"], optional = true } thiserror.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/cdk-sqlite/README.md b/crates/cdk-sqlite/README.md index ce1b27005..db663ddb1 100644 --- a/crates/cdk-sqlite/README.md +++ b/crates/cdk-sqlite/README.md @@ -13,6 +13,7 @@ The following crate feature flags are available: |-------------|:-------:|------------------------------------| | `wallet` | Yes | Enable cashu wallet features | | `mint` | Yes | Enable cashu mint wallet features | +| `sqlcipher` | No | Enable encrypted database | ## Implemented [NUTs](https://github.com/cashubtc/nuts/): diff --git a/crates/cdk-sqlite/src/common.rs b/crates/cdk-sqlite/src/common.rs index 3aba92a71..c7a6b46a4 100644 --- a/crates/cdk-sqlite/src/common.rs +++ b/crates/cdk-sqlite/src/common.rs @@ -5,7 +5,10 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::{Error, Pool, Sqlite}; #[inline(always)] -pub async fn create_sqlite_pool(path: &str) -> Result, Error> { +pub async fn create_sqlite_pool( + path: &str, + #[cfg(feature = "sqlcipher")] password: String, +) -> Result, Error> { let db_options = SqliteConnectOptions::from_str(path)? .busy_timeout(Duration::from_secs(10)) .read_only(false) @@ -17,6 +20,9 @@ pub async fn create_sqlite_pool(path: &str) -> Result, Error> { .shared_cache(true) .create_if_missing(true); + #[cfg(feature = "sqlcipher")] + let db_options = db_options.pragma("key", password); + let pool = SqlitePoolOptions::new() .min_connections(1) .max_connections(1) diff --git a/crates/cdk-sqlite/src/mint/memory.rs b/crates/cdk-sqlite/src/mint/memory.rs index 6dd134db9..a9d0609a5 100644 --- a/crates/cdk-sqlite/src/mint/memory.rs +++ b/crates/cdk-sqlite/src/mint/memory.rs @@ -12,7 +12,10 @@ use super::MintSqliteDatabase; /// Creates a new in-memory [`MintSqliteDatabase`] instance pub async fn empty() -> Result { + #[cfg(not(feature = "sqlcipher"))] let db = MintSqliteDatabase::new(":memory:").await?; + #[cfg(feature = "sqlcipher")] + let db = MintSqliteDatabase::new(":memory:", "memory".to_string()).await?; db.migrate().await; Ok(db) } diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index a7f9d9f96..d2728fb8d 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -68,12 +68,25 @@ impl MintSqliteDatabase { } /// Create new [`MintSqliteDatabase`] + #[cfg(not(feature = "sqlcipher"))] pub async fn new>(path: P) -> Result { Ok(Self { pool: create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?).await?, }) } + /// Create new [`MintSqliteDatabase`] + #[cfg(feature = "sqlcipher")] + pub async fn new>(path: P, password: String) -> Result { + Ok(Self { + pool: create_sqlite_pool( + path.as_ref().to_str().ok_or(Error::InvalidDbPath)?, + password, + ) + .await?, + }) + } + /// Migrate [`MintSqliteDatabase`] pub async fn migrate(&self) { sqlx::migrate!("./src/mint/migrations") diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index a40a9f8ec..3f842b23c 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -33,12 +33,25 @@ pub struct WalletSqliteDatabase { impl WalletSqliteDatabase { /// Create new [`WalletSqliteDatabase`] + #[cfg(not(feature = "sqlcipher"))] pub async fn new>(path: P) -> Result { Ok(Self { pool: create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?).await?, }) } + /// Create new [`WalletSqliteDatabase`] + #[cfg(feature = "sqlcipher")] + pub async fn new>(path: P, password: String) -> Result { + Ok(Self { + pool: create_sqlite_pool( + path.as_ref().to_str().ok_or(Error::InvalidDbPath)?, + password, + ) + .await?, + }) + } + /// Migrate [`WalletSqliteDatabase`] pub async fn migrate(&self) { sqlx::migrate!("./src/wallet/migrations") @@ -954,3 +967,32 @@ fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result { unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, }) } + +#[cfg(test)] +mod tests { + use std::env::temp_dir; + + #[tokio::test] + #[cfg(feature = "sqlcipher")] + async fn test_sqlcipher() { + use super::*; + let path = std::env::temp_dir() + .to_path_buf() + .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4())); + let db = WalletSqliteDatabase::new(path, "password".to_string()) + .await + .unwrap(); + + db.migrate().await; + + // do something simple to test the database + let pk = PublicKey::from_hex( + "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104", + ) + .unwrap(); + let last_checked = 6969; + db.add_nostr_last_checked(pk, last_checked).await.unwrap(); + let res = db.get_nostr_last_checked(&pk).await.unwrap(); + assert_eq!(res, Some(last_checked)); + } +}