Ver Fonte

feat: Add support for sqlcipher

benthecarman há 1 mês atrás
pai
commit
40c53e83df

+ 3 - 0
.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,

+ 3 - 0
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

+ 12 - 0
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<String>,
     /// Path to working dir
     #[arg(short, long)]
     work_dir: Option<PathBuf>,
@@ -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;
 

+ 1 - 0
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"]

+ 3 - 0
crates/cdk-mintd/src/cli.rs

@@ -12,6 +12,9 @@ pub struct CLIArgs {
         required = false
     )]
     pub work_dir: Option<PathBuf>,
+    #[cfg(feature = "sqlcipher")]
+    #[arg(short, long, help = "Database password for sqlcipher", required = true)]
+    pub password: String,
     #[arg(
         short,
         long,

+ 3 - 0
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;
 

+ 2 - 0
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

+ 1 - 0
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/):
 

+ 7 - 1
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<Pool<Sqlite>, Error> {
+pub async fn create_sqlite_pool(
+    path: &str,
+    #[cfg(feature = "sqlcipher")] password: String,
+) -> Result<Pool<Sqlite>, 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<Pool<Sqlite>, 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)

+ 3 - 0
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<MintSqliteDatabase, database::Error> {
+    #[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)
 }

+ 13 - 0
crates/cdk-sqlite/src/mint/mod.rs

@@ -68,12 +68,25 @@ impl MintSqliteDatabase {
     }
 
     /// Create new [`MintSqliteDatabase`]
+    #[cfg(not(feature = "sqlcipher"))]
     pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
         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<P: AsRef<Path>>(path: P, password: String) -> Result<Self, Error> {
+        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")

+ 42 - 0
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<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
         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<P: AsRef<Path>>(path: P, password: String) -> Result<Self, Error> {
+        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<ProofInfo, Error> {
         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));
+    }
+}