diff --git a/Cargo.lock b/Cargo.lock index 92c66b067..32d8b8dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ dependencies = [ "coset", "getrandom", "hmac", + "js-sys", "log", "p256", "passkey", @@ -452,6 +453,7 @@ dependencies = [ "schemars", "security-framework", "serde", + "serde-wasm-bindgen", "serde_json", "serde_qs", "serde_repr", diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index 57cafc140..4b2f32474 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -14,7 +14,7 @@ repository.workspace = true license-file.workspace = true [features] -default = [] +#default = ["sqlite"] internal = [] # Internal testing methods no-memory-hardening = [ @@ -60,6 +60,7 @@ serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" serde_qs = ">=0.12.0, <0.14" serde_repr = ">=0.1.12, <0.2" +serde-wasm-bindgen = "0.6.5" sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" thiserror = ">=1.0.40, <2.0" @@ -71,6 +72,7 @@ wasm-bindgen = { version = "0.2.91", features = [ "serde-serialize", ], optional = true } wasm-bindgen-futures = "0.4.41" +js-sys = "0.3.69" [target.'cfg(all(not(target_os = "android"), not(target_arch="wasm32")))'.dependencies] diff --git a/crates/bitwarden-core/src/database/mod.rs b/crates/bitwarden-core/src/database/mod.rs index 816105e0c..047edf527 100644 --- a/crates/bitwarden-core/src/database/mod.rs +++ b/crates/bitwarden-core/src/database/mod.rs @@ -9,11 +9,16 @@ mod migrator; mod sqlite; #[cfg(feature = "sqlite")] pub type Database = sqlite::SqliteDatabase; +use serde::Serialize; +#[cfg(feature = "sqlite")] +pub use sqlite::Params; #[cfg(feature = "wasm")] mod wasm; #[cfg(all(not(feature = "sqlite"), feature = "wasm"))] pub type Database = wasm::WasmDatabase; +#[cfg(all(not(feature = "sqlite"), feature = "wasm"))] +pub use wasm::{Params, ToSql}; use thiserror::Error; @@ -57,4 +62,9 @@ pub trait DatabaseTrait { async fn set_version(&self, version: usize) -> Result<(), DatabaseError>; async fn execute_batch(&self, sql: &str) -> Result<(), DatabaseError>; + + /// Convenience method to prepare and execute a single SQL statement. + /// + /// On success, returns the number of rows that were changed or inserted or deleted. + async fn execute(&self, sql: &str, params: P) -> Result; } diff --git a/crates/bitwarden-core/src/database/sqlite.rs b/crates/bitwarden-core/src/database/sqlite.rs index bff607f88..184090935 100644 --- a/crates/bitwarden-core/src/database/sqlite.rs +++ b/crates/bitwarden-core/src/database/sqlite.rs @@ -1,4 +1,5 @@ use rusqlite::Connection; +pub use rusqlite::Params; use super::{migrator::Migrator, DatabaseError, DatabaseTrait}; @@ -78,6 +79,12 @@ impl DatabaseTrait for SqliteDatabase { Ok(()) } + + async fn execute(&self, sql: &str, params: P) -> Result { + self.conn.execute(sql, params)?; + + Ok(0) + } } #[cfg(test)] diff --git a/crates/bitwarden-core/src/database/wasm.rs b/crates/bitwarden-core/src/database/wasm/mod.rs similarity index 79% rename from crates/bitwarden-core/src/database/wasm.rs rename to crates/bitwarden-core/src/database/wasm/mod.rs index 1264100e9..18e623ec0 100644 --- a/crates/bitwarden-core/src/database/wasm.rs +++ b/crates/bitwarden-core/src/database/wasm/mod.rs @@ -1,3 +1,7 @@ +mod params; +pub use params::{Params, ToSql}; + +use serde::Serialize; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use super::{DatabaseError, DatabaseTrait}; @@ -18,7 +22,10 @@ extern "C" { async fn set_version(this: &SqliteDatabase, version: u32); #[wasm_bindgen(method)] - async fn execute(this: &SqliteDatabase, sql: &str); + async fn execute_batch(this: &SqliteDatabase, sql: &str); + + #[wasm_bindgen(method)] + async fn execute(this: &SqliteDatabase, sql: &str, params: JsValue); } impl core::fmt::Debug for SqliteDatabase { @@ -39,7 +46,7 @@ impl WasmDatabase { pub async fn new() -> Result { let db: SqliteDatabase = SqliteDatabase::factory("test").await.into(); - db.execute( + db.execute_batch( "CREATE TABLE IF NOT EXISTS ciphers ( id TEXT PRIMARY KEY, value TEXT NOT NULL @@ -72,8 +79,14 @@ impl DatabaseTrait for WasmDatabase { } async fn execute_batch(&self, sql: &str) -> Result<(), DatabaseError> { - self.db.execute(sql).await; + self.db.execute_batch(sql).await; Ok(()) } + + async fn execute(&self, sql: &str, params: P) -> Result { + self.db.execute(sql, params.to_sql()).await; + + Ok(0) + } } diff --git a/crates/bitwarden-core/src/database/wasm/params.rs b/crates/bitwarden-core/src/database/wasm/params.rs new file mode 100644 index 000000000..54c0f5c70 --- /dev/null +++ b/crates/bitwarden-core/src/database/wasm/params.rs @@ -0,0 +1,71 @@ +use uuid::Uuid; +use wasm_bindgen::JsValue; + +// Borrowed from Rusqlite +pub trait ToSql { + fn to_sql(&self) -> JsValue; +} +impl ToSql for u8 { + fn to_sql(&self) -> JsValue { + JsValue::from_f64(*self as f64) + } +} +impl ToSql for String { + fn to_sql(&self) -> JsValue { + JsValue::from_str(self) + } +} +impl ToSql for Uuid { + fn to_sql(&self) -> JsValue { + JsValue::from_str(&self.to_string()) + } +} + +pub trait Params { + fn to_sql(&self) -> JsValue; +} +impl Params for [&(dyn ToSql + Send + Sync); 0] { + fn to_sql(&self) -> JsValue { + JsValue::NULL + } +} +impl Params for &[&dyn ToSql] { + fn to_sql(&self) -> JsValue { + let array = js_sys::Array::new(); + for item in *self { + array.push(&item.to_sql()); + } + array.into() + } +} +impl Params for &[(&str, &dyn ToSql)] { + fn to_sql(&self) -> JsValue { + let object = js_sys::Object::new(); + for (key, value) in *self { + js_sys::Reflect::set(&object, &JsValue::from_str(key), &value.to_sql()).unwrap(); + } + object.into() + } +} + +#[macro_export] +macro_rules! params { + () => { + &[] as &[&dyn $crate::ToSql] + }; + ($($param:expr),+ $(,)?) => { + &[$(&$param as &dyn $crate::ToSql),+] as &[&dyn $crate::ToSql] + }; +} + +#[macro_export] +macro_rules! named_params { + () => { + &[] as &[(&str, &dyn $crate::ToSql)] + }; + // Note: It's a lot more work to support this as part of the same macro as + // `params!`, unfortunately. + ($($param_name:literal: $param_val:expr),+ $(,)?) => { + &[$(($param_name, &$param_val as &dyn $crate::ToSql)),+] as &[(&str, &dyn $crate::ToSql)] + }; +} diff --git a/crates/bitwarden-core/src/lib.rs b/crates/bitwarden-core/src/lib.rs index f2a6b4e9b..d6e86cb65 100644 --- a/crates/bitwarden-core/src/lib.rs +++ b/crates/bitwarden-core/src/lib.rs @@ -13,7 +13,7 @@ pub use error::Error; pub mod mobile; pub use error::{MissingFieldError, VaultLocked}; mod database; -pub use database::{Database, DatabaseError, DatabaseTrait}; +pub use database::{Database, DatabaseError, DatabaseTrait, Params, ToSql}; #[cfg(feature = "internal")] pub mod platform; #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs index 3c11bb62d..ad0865ac0 100644 --- a/crates/bitwarden-json/src/client.rs +++ b/crates/bitwarden-json/src/client.rs @@ -54,7 +54,7 @@ impl Client { let client = &self.0; - let ciphers: Vec = (0..70000).map(|_| Cipher { + let ciphers: Vec = (0..10).map(|_| Cipher { id: Some(Uuid::new_v4()), organization_id: None, folder_id: None, diff --git a/crates/bitwarden-vault/src/cipher/repository.rs b/crates/bitwarden-vault/src/cipher/repository.rs index 3ca06e46e..519d64a02 100644 --- a/crates/bitwarden-vault/src/cipher/repository.rs +++ b/crates/bitwarden-vault/src/cipher/repository.rs @@ -1,6 +1,8 @@ use std::sync::{Arc, Mutex}; -use bitwarden_core::{require, Database, DatabaseError, DatabaseTrait, Error}; +use bitwarden_core::{ + named_params, params, require, Database, DatabaseError, DatabaseTrait, Error, +}; use idb::{DatabaseEvent, Factory, KeyPath, ObjectStoreParams, TransactionMode}; use serde::Serialize; use serde_wasm_bindgen::Serializer; @@ -65,6 +67,7 @@ impl CipherRepository { .await?; */ + /* // Get a factory instance from global scope let factory = Factory::new().unwrap(); @@ -111,6 +114,7 @@ impl CipherRepository { // Commit the transaction transaction.commit().unwrap().await.unwrap(); + */ //let tx = guard.conn.transaction()?; //{ @@ -123,19 +127,17 @@ impl CipherRepository { ", )?;*/ - /* for cipher in ciphers { let id = require!(cipher.id); let serialized = serde_json::to_string(&cipher)?; guard - .execute_batch(&format!( - "INSERT INTO ciphers (id, value) VALUES ('{}', '{}')", - id, "abc" - )) + .execute( + "INSERT INTO ciphers (id, value) VALUES (:id, :data)", + named_params! {":id": id, ":data": serialized}, + ) .await?; } - */ //} //tx.commit()?; diff --git a/languages/js/sqlite-test/src/main.js b/languages/js/sqlite-test/src/main.js index 7b651b585..1da0fdb99 100644 --- a/languages/js/sqlite-test/src/main.js +++ b/languages/js/sqlite-test/src/main.js @@ -38,8 +38,24 @@ class SqliteDatabase { console.log("Version", version); } - async execute(sql) { + async execute_batch(sql) { console.log(sql); + // localStorage.setItem("sql", sql); + await sqlite3.exec(this.db, sql); + } + + + async execute(sql, params) { + console.log(sql, params); + for await (const stmt of sqlite3.statements(this.db, sql)) { + let rc = sqlite3.bind_collection(stmt, params); + + while ((rc = await sqlite3.step(stmt)) !== SQLite.SQLITE_DONE) { +console.log(rc); + } + + } + // localStorage.setItem("sql", sql); // await sqlite3.exec(this.db, sql); }