diff --git a/Cargo.lock b/Cargo.lock index 01dcd7c1..9a825374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,6 +301,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -337,6 +343,35 @@ version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +[[package]] +name = "bip32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e141fb0f8be1c7b45887af94c88b182472b57c96b56773250ae00cd6a14a164" +dependencies = [ + "bs58", + "hmac", + "k256", + "once_cell", + "pbkdf2", + "rand_core", + "ripemd", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes 0.11.0", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin" version = "0.29.2" @@ -477,6 +512,15 @@ dependencies = [ "serde_with 1.14.0", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2", +] + [[package]] name = "bstr" version = "1.9.1" @@ -687,6 +731,18 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -864,6 +920,19 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "either" version = "1.10.0" @@ -873,6 +942,24 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -947,6 +1034,16 @@ dependencies = [ "tower", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "finl_unicode" version = "1.2.0" @@ -1109,6 +1206,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1170,6 +1268,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.25" @@ -1627,6 +1736,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1923,11 +2044,15 @@ version = "0.2.1" dependencies = [ "anyhow", "async-trait", + "bip32", + "bip39", "dirs", "gloo-net", + "hex", "lightning-invoice 0.29.0", "mockall", "moksha-core", + "rand", "reqwest 0.12.2", "rexie", "secp256k1 0.28.2", @@ -2195,6 +2320,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2685,6 +2820,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -2715,6 +2860,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + [[package]] name = "rsa" version = "0.9.6" @@ -2899,6 +3053,19 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.24.3" @@ -4019,9 +4186,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] diff --git a/moksha-core/src/dhke.rs b/moksha-core/src/dhke.rs index aa83a713..268ea4d9 100644 --- a/moksha-core/src/dhke.rs +++ b/moksha-core/src/dhke.rs @@ -89,6 +89,7 @@ impl Dhke { .ok_or(MokshaCoreError::NoValidPointFound) } + // FIXME: use SecretKey instead of &[u8] for blinding factor pub fn step1_alice( &self, secret_msg: impl Into, diff --git a/moksha-core/src/primitives.rs b/moksha-core/src/primitives.rs index 98bd7bad..96dc4e82 100644 --- a/moksha-core/src/primitives.rs +++ b/moksha-core/src/primitives.rs @@ -136,6 +136,16 @@ pub enum CurrencyUnit { Usd, } +impl From for CurrencyUnit { + fn from(unit: String) -> Self { + match unit.to_lowercase().as_str() { + "sat" => Self::Sat, + "usd" => Self::Usd, + _ => panic!("Unknown currency unit: {}", unit), + } + } +} + impl Display for CurrencyUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/moksha-wallet/Cargo.toml b/moksha-wallet/Cargo.toml index 3a4b74d1..5ad52ada 100644 --- a/moksha-wallet/Cargo.toml +++ b/moksha-wallet/Cargo.toml @@ -24,6 +24,10 @@ async-trait = "0.1.77" lightning-invoice = "0.29.0" url = "2.5.0" dirs = "5.0.1" +bip32 = { version = "0.5.1", features = ["secp256k1", "std"] } +bip39 = "2.0.0" +hex = "0.4.3" +rand = "0.8.5" [target.'cfg(target_family = "wasm")'.dependencies] gloo-net = { version = "0.5.0" } diff --git a/moksha-wallet/migrations/20230530061910_init.sql b/moksha-wallet/migrations/20230530061910_init.sql index 8c060bed..836fba1a 100644 --- a/moksha-wallet/migrations/20230530061910_init.sql +++ b/moksha-wallet/migrations/20230530061910_init.sql @@ -9,8 +9,12 @@ CREATE TABLE IF NOT EXISTS proofs ( ); CREATE TABLE IF NOT EXISTS keysets ( - id TEXT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, mint_url TEXT NOT NULL, + keyset_id TEXT NOT NULL, + currency_unit TEXT NOT NULL, active BOOL DEFAULT TRUE, - UNIQUE (id, mint_url) + last_index INTEGER, + public_keys JSON CHECK (json_valid(public_keys)), + UNIQUE (keyset_id, mint_url) ); \ No newline at end of file diff --git a/moksha-wallet/migrations/20240329082342_deterministic_secrets.sql b/moksha-wallet/migrations/20240329082342_deterministic_secrets.sql new file mode 100644 index 00000000..e8c1304a --- /dev/null +++ b/moksha-wallet/migrations/20240329082342_deterministic_secrets.sql @@ -0,0 +1,6 @@ +-- Add migration script here +CREATE TABLE seed ( + id INTEGER PRIMARY KEY CHECK (id = 1), + seed_words TEXT NOT NULL + -- other columns +); \ No newline at end of file diff --git a/moksha-wallet/src/error.rs b/moksha-wallet/src/error.rs index 9075fa04..3cb1ce89 100644 --- a/moksha-wallet/src/error.rs +++ b/moksha-wallet/src/error.rs @@ -42,6 +42,7 @@ pub enum MokshaWalletError { #[cfg(not(target_arch = "wasm32"))] #[error("Sqlite Error {0}")] Sqlite(#[from] sqlx::sqlite::SqliteError), + #[error("Utf8 Error {0}")] Utf8(#[from] FromUtf8Error), @@ -62,4 +63,19 @@ pub enum MokshaWalletError { #[error("Unsupported version: Only mints with /v1 api are supported")] UnsupportedApiVersion, + + #[error("Bip32Error {0}")] + Bip32(#[from] bip32::Error), + + #[error("Bip39Error {0}")] + Bip39(#[from] bip39::Error), + + #[error("Secp256k1 {0}")] + Secp256k1(#[from] secp256k1::Error), + + #[error("Primarykey not set for keyset")] + IdNotSet, + + #[error("Found multiple seeds in the database. This is not supported.")] + MultipleSeeds, } diff --git a/moksha-wallet/src/lib.rs b/moksha-wallet/src/lib.rs index 24618146..543ec49f 100644 --- a/moksha-wallet/src/lib.rs +++ b/moksha-wallet/src/lib.rs @@ -4,4 +4,5 @@ pub mod config_path; pub mod error; pub mod http; pub mod localstore; +pub mod secret; pub mod wallet; diff --git a/moksha-wallet/src/localstore/mod.rs b/moksha-wallet/src/localstore/mod.rs index 3f754ee1..19a9e3e8 100644 --- a/moksha-wallet/src/localstore/mod.rs +++ b/moksha-wallet/src/localstore/mod.rs @@ -1,5 +1,9 @@ +use std::collections::HashMap; + use async_trait::async_trait; -use moksha_core::proof::Proofs; +use moksha_core::{primitives::CurrencyUnit, proof::Proofs}; +use secp256k1::PublicKey; +use url::Url; use crate::error::MokshaWalletError; @@ -11,8 +15,36 @@ pub mod rexie; #[derive(Debug, Clone)] pub struct WalletKeyset { - pub id: String, - pub mint_url: String, + /// primary key + pub id: Option, + pub keyset_id: String, + pub mint_url: Url, + pub currency_unit: CurrencyUnit, + /// last index used for deriving keys from the master key + pub last_index: u64, + pub public_keys: HashMap, + pub active: bool, +} + +impl WalletKeyset { + pub fn new( + keyset_id: &str, + mint_url: &Url, + currency_unit: &CurrencyUnit, + last_index: u64, + public_keys: HashMap, + active: bool, + ) -> Self { + Self { + id: None, + keyset_id: keyset_id.to_owned(), + mint_url: mint_url.to_owned(), + currency_unit: currency_unit.clone(), + last_index, + public_keys, + active, + } + } } #[cfg(not(target_arch = "wasm32"))] @@ -35,8 +67,32 @@ pub trait LocalStore { tx: &mut sqlx::Transaction, ) -> Result; - async fn get_keysets(&self) -> Result, MokshaWalletError>; - async fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError>; + async fn get_keysets( + &self, + tx: &mut sqlx::Transaction, + ) -> Result, MokshaWalletError>; + async fn upsert_keyset( + &self, + tx: &mut sqlx::Transaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError>; + + async fn update_keyset_last_index( + &self, + tx: &mut sqlx::Transaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError>; + + async fn add_seed( + &self, + tx: &mut sqlx::Transaction, + seed_words: &str, + ) -> Result<(), MokshaWalletError>; + + async fn get_seed( + &self, + tx: &mut sqlx::Transaction, + ) -> Result, MokshaWalletError>; } #[cfg(target_arch = "wasm32")] @@ -68,6 +124,59 @@ pub trait LocalStore { ) -> Result<(), MokshaWalletError>; async fn get_proofs(&self, tx: &mut RexieTransaction) -> Result; - async fn get_keysets(&self) -> Result, MokshaWalletError>; - async fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError>; + async fn get_keysets( + &self, + _tx: &mut RexieTransaction, + ) -> Result, MokshaWalletError>; + + async fn upsert_keyset( + &self, + _tx: &mut RexieTransaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError>; + + async fn update_keyset_last_index( + &self, + _tx: &mut RexieTransaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError>; + + async fn add_seed( + &self, + _tx: &mut RexieTransaction, + seed_words: &str, + ) -> Result<(), MokshaWalletError>; + + async fn get_seed( + &self, + _tx: &mut RexieTransaction, + ) -> Result, MokshaWalletError>; +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use secp256k1::PublicKey; + + fn generate_test_map() -> HashMap { + let mut map = HashMap::new(); + let secp = secp256k1::Secp256k1::new(); + + for i in 0..10 { + let secret_key = secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + map.insert(i, public_key); + } + + map + } + + #[test] + fn test_() { + //let x: HashMap, RandomState>; + let data = generate_test_map(); + let json = serde_json::to_string(&data).unwrap(); + println!("{:?}", json); + } } diff --git a/moksha-wallet/src/localstore/rexie.rs b/moksha-wallet/src/localstore/rexie.rs index 1a08e5fa..1f72368b 100644 --- a/moksha-wallet/src/localstore/rexie.rs +++ b/moksha-wallet/src/localstore/rexie.rs @@ -110,19 +110,41 @@ impl LocalStore for RexieLocalStore { Ok(()) } - async fn get_keysets(&self) -> std::result::Result, MokshaWalletError> { - // FIXME todo implement - Ok(vec![WalletKeyset { - id: "id".to_string(), - mint_url: "mint_url".to_string(), - }]) + async fn get_keysets( + &self, + _tx: &mut RexieTransaction, + ) -> std::result::Result, MokshaWalletError> { + todo!() } - async fn add_keyset( + async fn upsert_keyset( &self, + _tx: &mut RexieTransaction, _keyset: &WalletKeyset, ) -> std::result::Result<(), MokshaWalletError> { - // FIXME todo implement - Ok(()) + todo!() + } + + async fn update_keyset_last_index( + &self, + _tx: &mut RexieTransaction, + _keyset: &WalletKeyset, + ) -> std::result::Result<(), MokshaWalletError> { + todo!() + } + + async fn add_seed( + &self, + _tx: &mut RexieTransaction, + _seed_words: &str, + ) -> std::result::Result<(), MokshaWalletError> { + todo!() + } + + async fn get_seed( + &self, + _tx: &mut RexieTransaction, + ) -> std::result::Result, MokshaWalletError> { + todo!() } } diff --git a/moksha-wallet/src/localstore/sqlite.rs b/moksha-wallet/src/localstore/sqlite.rs index c4cfb58e..d7dab1d0 100644 --- a/moksha-wallet/src/localstore/sqlite.rs +++ b/moksha-wallet/src/localstore/sqlite.rs @@ -1,5 +1,9 @@ +use std::collections::HashMap; + use async_trait::async_trait; use moksha_core::proof::{Proof, Proofs}; +use secp256k1::PublicKey; +use url::Url; use crate::error::MokshaWalletError; use crate::localstore::{LocalStore, WalletKeyset}; @@ -88,32 +92,103 @@ impl LocalStore for SqliteLocalStore { .into()) } - async fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError> { - sqlx::query( - r#"INSERT INTO keysets (id, mint_url) VALUES ($1, $2); - "#, - ) - .bind(keyset.id.to_owned()) - .bind(keyset.mint_url.to_owned()) - .execute(&self.pool) + async fn upsert_keyset( + &self, + tx: &mut sqlx::Transaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError> { + let row: (i64,) = sqlx::query_as( + r#"INSERT INTO keysets (keyset_id, mint_url, currency_unit, last_index, public_keys, active) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(keyset_id, mint_url) DO UPDATE SET currency_unit = $3, last_index = $4, public_keys = $5, active = $6; + SELECT last_insert_rowid() as id; + "#) + .bind(keyset.keyset_id.to_owned()) + .bind(keyset.mint_url.as_str()) + .bind(keyset.currency_unit.to_string()) + .bind(keyset.last_index as i64) + .bind(serde_json::to_string(&keyset.public_keys)?) + .bind(keyset.active) + .fetch_one(&mut **tx) .await?; + // FIXME do we need the id? Ok(()) } - async fn get_keysets(&self) -> Result, MokshaWalletError> { + async fn get_keysets( + &self, + tx: &mut sqlx::Transaction, + ) -> Result, MokshaWalletError> { let rows = sqlx::query("SELECT * FROM keysets;") - .fetch_all(&self.pool) + .fetch_all(&mut **tx) .await?; Ok(rows .iter() .map(|row| { - let id = row.get(0); - let mint_url: String = row.get(1); - Ok(WalletKeyset { id, mint_url }) + let id: i64 = row.get(0); + let mint_url: Url = Url::parse(row.get(1)).expect("invalid url in localstore"); + let keyset_id: String = row.get(2); + let currency_unit: String = row.get(3); + let active: bool = row.get(4); + let last_index: i64 = row.get(5); + let public_keys: HashMap = + serde_json::from_str(row.get(6)).expect("invalid json in localstore"); + Ok(WalletKeyset { + id: Some(id as u64), + mint_url, + keyset_id, + currency_unit: currency_unit.into(), + active, + last_index: last_index as u64, + public_keys, + }) }) .collect::, SqliteError>>()?) } + + async fn update_keyset_last_index( + &self, + tx: &mut sqlx::Transaction, + keyset: &WalletKeyset, + ) -> Result<(), MokshaWalletError> { + let id = match keyset.id { + None => return Err(MokshaWalletError::IdNotSet), + Some(id) => id as i64, + }; + + sqlx::query(r#"UPDATE keysets SET last_index = $1 WHERE id = $2;"#) + .bind(keyset.last_index as i64) + .bind(id) + .execute(&mut **tx) + .await?; + Ok(()) + } + + async fn add_seed( + &self, + tx: &mut sqlx::Transaction, + seed_words: &str, + ) -> Result<(), MokshaWalletError> { + sqlx::query("INSERT INTO seed (seed_words) VALUES ($1);") + .bind(seed_words) + .execute(&mut **tx) + .await?; + Ok(()) + } + + async fn get_seed( + &self, + tx: &mut sqlx::Transaction, + ) -> Result, MokshaWalletError> { + let row = sqlx::query("SELECT seed_words FROM seed;") + .fetch_all(&mut **tx) + .await?; + match row.len() { + 0 => Ok(None), + 1 => Ok(Some(row[0].get(0))), + _ => Err(MokshaWalletError::MultipleSeeds), + } + } } impl SqliteLocalStore { diff --git a/moksha-wallet/src/secret.rs b/moksha-wallet/src/secret.rs new file mode 100644 index 00000000..6156bf3d --- /dev/null +++ b/moksha-wallet/src/secret.rs @@ -0,0 +1,151 @@ +use crate::error::MokshaWalletError; +use std::str::FromStr; + +use bip32::{Seed, XPrv}; +use bip39::Mnemonic; +use rand::Rng; +use secp256k1::SecretKey; + +enum DerivationType { + Secret = 0, + Blinding = 1, +} + +pub struct DeterministicSecret { + pub seed: Seed, +} + +impl Clone for DeterministicSecret { + fn clone(&self) -> Self { + Self { + seed: Seed::new(*self.seed.as_bytes()), + } + } +} + +impl DeterministicSecret { + pub fn from_seed_words(seed_words: &str) -> Result { + let mnemonic = Mnemonic::from_str(seed_words)?; + let seed = Seed::new(mnemonic.to_seed("")); + Ok(Self { seed }) + } + + pub fn from_random_seed() -> Result { + let mut rng = rand::thread_rng(); + let entropy: [u8; 16] = rng.gen(); // 16 bytes for 12 words mnemonic + let mnemonic = Mnemonic::from_entropy(&entropy)?; + let seed = Seed::new(mnemonic.to_seed("")); + Ok(Self { seed }) + } + + pub fn generate_random_seed_words() -> Result { + let mut rng = rand::thread_rng(); + let entropy: [u8; 16] = rng.gen(); // 16 bytes for 12 words mnemonic + let mnemonic = Mnemonic::from_entropy(&entropy)?; + Ok(mnemonic.word_iter().collect::>().join(" ")) + } + + fn derive_private_key( + &self, + keyset_id: u32, + counter: u32, + secret_or_blinding: DerivationType, + ) -> Result, MokshaWalletError> { + let secret_or_blinding = secret_or_blinding as u32; + let derivation_path = format!("m/129372'/0'/{keyset_id}'/{counter}'/{secret_or_blinding}"); + let derivation_path = bip32::DerivationPath::from_str(&derivation_path)?; + let key = XPrv::derive_from_path(&self.seed, &derivation_path)?; + Ok(key.private_key().to_bytes().to_vec()) + } + + pub fn derive_secret(&self, keyset_id: u32, counter: u32) -> Result { + let key = self.derive_private_key(keyset_id, counter, DerivationType::Secret)?; + Ok(hex::encode(key)) + } + + pub fn derive_range( + &self, + keyset_id: u32, + start: u32, + length: u32, + ) -> Result, MokshaWalletError> { + Ok((start..start + length) + .map(|i| { + let key = self.derive_secret(keyset_id, i).unwrap(); + let blinding_factor = self.derive_blinding_factor(keyset_id, i).unwrap(); + (key, blinding_factor) + }) + .collect::>()) + } + + pub fn derive_blinding_factor( + &self, + keyset_id: u32, + counter: u32, + ) -> Result { + let key = self.derive_private_key(keyset_id, counter, DerivationType::Blinding)?; + Ok(SecretKey::from_slice(&key)?) + } +} + +pub fn convert_hex_to_int(keyset_id_hex: &str) -> anyhow::Result { + let bytes = hex::decode(keyset_id_hex)?; + let bytes_array: [u8; 8] = bytes[0..8].try_into()?; + let num = u64::from_be_bytes(bytes_array); + Ok((num % (2u64.pow(31) - 1)) as u32) +} + +#[cfg(test)] +mod tests { + + use super::{convert_hex_to_int, DeterministicSecret}; + + #[test] + fn test_keyset_id_conversion() -> anyhow::Result<()> { + let int_value = convert_hex_to_int("009a1f293253e41e")?; + assert_eq!(864559728, int_value); + Ok(()) + } + + #[test] + fn test_generate_seed_words() -> anyhow::Result<()> { + let seed_words = DeterministicSecret::generate_random_seed_words()?; + println!("{}", seed_words); + assert_eq!(12, seed_words.split_whitespace().count()); + Ok(()) + } + + #[test] + fn test_secret_derivation() -> anyhow::Result<()> { + let phrase = + "half depart obvious quality work element tank gorilla view sugar picture humble"; + let deterministic_secret = DeterministicSecret::from_seed_words(phrase)?; + + let secrets = [ + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + ]; + + for (i, secret) in secrets.iter().enumerate() { + let key = deterministic_secret.derive_secret(864559728, i as u32)?; + assert_eq!(secret.to_owned(), key); + } + + let blinding_factors = [ + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + ]; + + for (i, factor) in blinding_factors.iter().enumerate() { + let key = deterministic_secret.derive_blinding_factor(864559728, i as u32)?; + assert_eq!(factor.to_owned(), hex::encode(&key[..])); + } + Ok(()) + } +} diff --git a/moksha-wallet/src/wallet.rs b/moksha-wallet/src/wallet.rs index 561eba85..8f9cf4ec 100644 --- a/moksha-wallet/src/wallet.rs +++ b/moksha-wallet/src/wallet.rs @@ -20,6 +20,7 @@ use crate::{ error::MokshaWalletError, http::CrossPlatformHttpClient, localstore::{LocalStore, WalletKeyset}, + secret::{self, DeterministicSecret}, }; use lightning_invoice::Bolt11Invoice as LNInvoice; use std::str::FromStr; @@ -36,6 +37,7 @@ where dhke: Dhke, localstore: L, mint_url: Url, + secret: DeterministicSecret, } pub struct WalletBuilder @@ -85,23 +87,33 @@ where return Err(MokshaWalletError::UnsupportedApiVersion); } - let load_keysets = localstore.get_keysets().await?; + let mut tx = localstore.begin_tx().await?; + + let seed_words = localstore.get_seed(&mut tx).await?; + if seed_words.is_none() { + let seed = DeterministicSecret::generate_random_seed_words()?; + println!("Generated new seed: {:?}", seed); + localstore.add_seed(&mut tx, &seed).await?; + } let mint_keysets = client.get_keysets(&mint_url).await?; - if load_keysets.is_empty() { - let wallet_keysets = mint_keysets + + for m in mint_keysets.keysets.iter() { + let public_keys = client + .get_keys_by_id(&mint_url, m.id.clone()) + .await? .keysets - .iter() - .map(|m| WalletKeyset { - id: m.clone().id, - mint_url: mint_url.to_string(), - }) - .collect::>(); - - for wkeyset in wallet_keysets { - localstore.add_keyset(&wkeyset).await?; - } + .into_iter() + .find(|k| k.id == m.id) + .expect("no valid keyset found") + .keys + .clone(); + + let wallet_keyset = WalletKeyset::new(&m.id, &mint_url, &m.unit, 0, public_keys, true); + localstore.upsert_keyset(&mut tx, &wallet_keyset).await?; } + let seed = localstore.get_seed(&mut tx).await?.expect("seed not found"); + tx.commit().await?; // FIXME store all keysets let keys = client.get_keys(&mint_url).await?; @@ -124,6 +136,7 @@ where key_response.clone(), localstore, mint_url, + DeterministicSecret::from_seed_words(&seed)?, )) } } @@ -149,6 +162,7 @@ where key_response: KeyResponse, localstore: L, mint_url: Url, + secret: DeterministicSecret, ) -> Self { Self { client, @@ -157,6 +171,7 @@ where dhke: Dhke::new(), localstore, mint_url, + secret, } } @@ -416,6 +431,38 @@ where Ok(melt_response) } + async fn create_secrets( + &self, + amount: u32, + ) -> Result, MokshaWalletError> { + let keyset_id = self.keyset_id.clone().id; + let keyset_id_int = secret::convert_hex_to_int(&keyset_id).unwrap(); // FIXME + + let mut tx = self.localstore.begin_tx().await?; + let all_keysets = self.localstore.get_keysets(&mut tx).await?; + let keyset = all_keysets + .iter() + .find(|k| k.keyset_id == keyset_id) + .expect("keyset not found"); + + let start_index = (keyset.last_index + 1) as u32; + let secret_range = self + .secret + .derive_range(keyset_id_int, start_index, amount)?; + + self.localstore + .update_keyset_last_index( + &mut tx, + &WalletKeyset { + last_index: (start_index + amount) as u64, + ..keyset.clone() + }, + ) + .await?; + tx.commit().await?; + Ok(secret_range) + } + pub async fn split_tokens( &self, tokens: &TokenV3, @@ -423,13 +470,17 @@ where ) -> Result<(TokenV3, TokenV3), MokshaWalletError> { let total_token_amount = tokens.total_amount(); let first_amount: Amount = (total_token_amount - splt_amount.0).into(); - let first_secrets = first_amount.split().create_secrets(); + let first_secrets = self + .create_secrets(first_amount.split().len() as u32) + .await?; let first_outputs = self.create_blinded_messages(first_amount, &first_secrets)?; // ############################################################################ let second_amount = splt_amount.clone(); - let second_secrets = second_amount.split().create_secrets(); + let second_secrets = self + .create_secrets(second_amount.split().len() as u32) + .await?; let second_outputs = self.create_blinded_messages(second_amount, &second_secrets)?; let mut total_outputs = vec![]; @@ -453,6 +504,8 @@ where let secrets = [first_secrets, second_secrets].concat(); let outputs = [first_outputs, second_outputs].concat(); + let secrets = secrets.into_iter().map(|(s, _)| s).collect::>(); + let proofs = self .create_proofs_from_blinded_signatures(split_result.signatures, secrets, outputs)? .proofs(); @@ -520,13 +573,42 @@ where quote_id: String, ) -> Result { let split_amount = amount.split(); - let secrets = split_amount.create_secrets(); + + // FIXME cleanup code + let keyset_id = self.keyset_id.clone().id; + let keyset_id_int = secret::convert_hex_to_int(&keyset_id).unwrap(); // FIXME + + let mut tx = self.localstore.begin_tx().await?; + let all_keysets = self.localstore.get_keysets(&mut tx).await?; + let keyset = all_keysets + .iter() + .find(|k| k.keyset_id == keyset_id) + .expect("keyset not found"); + + let start_index = (keyset.last_index + 1) as u32; + let secret_range = + self.secret + .derive_range(keyset_id_int, start_index, split_amount.len() as u32)?; + + self.localstore + .update_keyset_last_index( + &mut tx, + &WalletKeyset { + last_index: (start_index + split_amount.len() as u32) as u64, + ..keyset.clone() + }, + ) + .await?; + tx.commit().await?; let blinded_messages = split_amount .into_iter() - .zip(secrets.clone()) - .map(|(amount, secret)| { - let (b_, alice_secret_key) = self.dhke.step1_alice(secret, None).unwrap(); // FIXME + .zip(secret_range) + .map(|(amount, (secret, blinding_factor))| { + let (b_, alice_secret_key) = self + .dhke + .step1_alice(&secret, Some(&blinding_factor.secret_bytes())) + .unwrap(); // FIXME ( BlindedMessage { amount, @@ -534,9 +616,10 @@ where id: self.keyset_id.clone().id, }, alice_secret_key, + secret, ) }) - .collect::>(); + .collect::>(); let signatures = match payment_method { PaymentMethod::Bolt11 => { @@ -548,7 +631,7 @@ where blinded_messages .clone() .into_iter() - .map(|(msg, _)| msg) + .map(|(msg, _, _)| msg) .collect::>(), ) .await?; @@ -563,7 +646,7 @@ where blinded_messages .clone() .into_iter() - .map(|(msg, _)| msg) + .map(|(msg, _, _)| msg) .collect::>(), ) .await?; @@ -574,17 +657,10 @@ where // step 3: unblind signatures let current_keyset_id = self.keyset_id.clone().id; // FIXME - let private_keys = blinded_messages - .clone() - .into_iter() - .map(|(_, secret)| secret) - .collect::>(); - let proofs = signatures .iter() - .zip(private_keys) - .zip(secrets) - .map(|((p, priv_key), secret)| { + .zip(blinded_messages) + .map(|(p, (_, priv_key, secret))| { let key = self .keyset .keys @@ -610,15 +686,18 @@ where fn create_blinded_messages( &self, amount: Amount, - secrets: &[String], + secrets_factors: &Vec<(String, SecretKey)>, ) -> Result, MokshaWalletError> { let split_amount = amount.split(); Ok(split_amount .into_iter() - .zip(secrets) - .map(|(amount, secret)| { - let (b_, alice_secret_key) = self.dhke.step1_alice(secret, None).unwrap(); // FIXME + .zip(secrets_factors) + .map(|(amount, (secret, blinding_factor))| { + let (b_, alice_secret_key) = self + .dhke + .step1_alice(secret, Some(&blinding_factor.secret_bytes())) + .unwrap(); // FIXME ( BlindedMessage { amount, @@ -694,7 +773,8 @@ mod tests { id: keys.keyset_id.clone(), unit: CurrencyUnit::Sat, }; - let keys_response = KeysResponse::new(key_response); + let keys_response = KeysResponse::new(key_response.clone()); + let keys_by_id_response = keys_response.clone(); let keysets = V1Keysets::new(keys.keyset_id, CurrencyUnit::Sat, true); let mut client = MockCashuClient::default(); @@ -704,6 +784,9 @@ mod tests { client .expect_get_keysets() .returning(move |_| Ok(keysets.clone())); + client + .expect_get_keys_by_id() + .returning(move |_, _| Ok(keys_by_id_response.clone())); client.expect_is_v1_supported().returning(move |_| Ok(true)); client }