diff --git a/packages/sdk/lib/Cargo.lock b/packages/sdk/lib/Cargo.lock index e599d97388..e19d9caffe 100644 --- a/packages/sdk/lib/Cargo.lock +++ b/packages/sdk/lib/Cargo.lock @@ -48,6 +48,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -84,6 +98,17 @@ version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash 0.4.2", +] + [[package]] name = "ark-bls12-381" version = "0.3.0" @@ -267,9 +292,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" [[package]] name = "bech32" @@ -313,6 +338,20 @@ dependencies = [ "serde", ] +[[package]] +name = "bip0039" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef0f0152ec5cf17f49a5866afaa3439816207fd4f0a224c0211ffaf5e278426" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.10.1", + "rand 0.8.5", + "sha2 0.10.8", + "unicode-normalization", + "zeroize", +] + [[package]] name = "bip0039" version = "0.12.0" @@ -423,6 +462,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" dependencies = [ "ff", + "group", + "pairing", "rand_core 0.6.4", "subtle", ] @@ -832,6 +873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1697,6 +1739,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -2769,6 +2821,19 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "masp_note_encryption" +version = "1.0.0" +source = "git+https://github.com/anoma/masp?tag=v1.1.0#f24691c0eb76909e3c15ae03aef294dccebd2df3" +dependencies = [ + "borsh", + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "masp_note_encryption" version = "1.0.0" @@ -2782,13 +2847,44 @@ dependencies = [ "subtle", ] +[[package]] +name = "masp_primitives" +version = "1.0.0" +source = "git+https://github.com/anoma/masp?tag=v1.1.0#f24691c0eb76909e3c15ae03aef294dccebd2df3" +dependencies = [ + "aes", + "bip0039 0.10.1", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "borsh", + "byteorder", + "ff", + "fpe", + "group", + "hex", + "incrementalmerkletree", + "jubjub 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "masp_note_encryption 1.0.0 (git+https://github.com/anoma/masp?tag=v1.1.0)", + "memuse", + "nonempty", + "num-traits 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.8.5", + "rand_core 0.6.4", + "sha2 0.10.8", + "subtle", + "zcash_encoding", +] + [[package]] name = "masp_primitives" version = "1.0.0" source = "git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe#0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe" dependencies = [ "aes", - "bip0039", + "bip0039 0.12.0", "bitvec", "blake2b_simd", "blake2s_simd", @@ -2802,7 +2898,7 @@ dependencies = [ "incrementalmerkletree", "jubjub 0.10.0 (git+https://github.com/heliaxdev/jubjub.git?rev=a373686962f4e9d0edb3b4716f86ff6bbd9aa86c)", "lazy_static", - "masp_note_encryption", + "masp_note_encryption 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "memuse", "nonempty", "num-traits 0.2.19 (git+https://github.com/heliaxdev/num-traits?rev=3f3657caa34b8e116fdf3f8a3519c4ac29f012fe)", @@ -2827,7 +2923,7 @@ dependencies = [ "itertools 0.11.0", "jubjub 0.10.0 (git+https://github.com/heliaxdev/jubjub.git?rev=a373686962f4e9d0edb3b4716f86ff6bbd9aa86c)", "lazy_static", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "rand_core 0.6.4", "redjubjub", "tracing", @@ -2887,25 +2983,31 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" name = "namada-sdk-wasm" version = "1.0.0" dependencies = [ + "aes-gcm", + "argon2", "async-trait", + "borsh-ext", "chrono", "console_error_panic_hook", "getrandom 0.2.15", "gloo-utils", "hex", "js-sys", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?tag=v1.1.0)", "namada_sdk", "namada_tx", + "password-hash 0.3.2", "rand 0.8.5", "rayon", "reqwest", "rexie", "serde", "serde_json", + "slip10_ed25519", "subtle-encoding", "tendermint-config 0.34.1", "thiserror", - "tiny-bip39 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny-bip39 1.0.0", "tokio", "wasm-bindgen", "wasm-bindgen-futures", @@ -2958,7 +3060,7 @@ dependencies = [ "indexmap 2.2.4", "k256", "lazy_static", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "namada_macros", "num-integer", "num-rational", @@ -3076,7 +3178,7 @@ dependencies = [ "ibc-derive", "ics23", "konst", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "namada_core", "namada_events", "namada_gas", @@ -3203,7 +3305,7 @@ dependencies = [ "init-once", "itertools 0.12.1", "lazy_static", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "masp_proofs", "namada_account", "namada_core", @@ -3243,7 +3345,7 @@ dependencies = [ "tempfile", "tendermint-rpc", "thiserror", - "tiny-bip39 0.8.2 (git+https://github.com/anoma/tiny-bip39.git?rev=bf0f6d8713589b83af7a917366ec31f5275c0e57)", + "tiny-bip39 0.8.2", "tokio", "toml 0.5.11", "tracing", @@ -3263,7 +3365,7 @@ dependencies = [ "futures", "itertools 0.12.1", "lazy_static", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "masp_proofs", "namada_account", "namada_controller", @@ -3390,7 +3492,7 @@ dependencies = [ "data-encoding", "either", "konst", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "namada_account", "namada_core", "namada_events", @@ -3474,7 +3576,7 @@ version = "0.46.0" source = "git+https://github.com/anoma/namada?rev=49a4a5d3260423df19ead14df82d18a51fa9b157#49a4a5d3260423df19ead14df82d18a51fa9b157" dependencies = [ "derivative", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "namada_core", "namada_events", "namada_gas", @@ -3494,7 +3596,7 @@ dependencies = [ "data-encoding", "derivation-path", "itertools 0.12.1", - "masp_primitives", + "masp_primitives 1.0.0 (git+https://github.com/anoma/masp?rev=0d0da3507a6f9ad135f00fd8201dc54c2f1d9efe)", "namada_core", "namada_ibc", "namada_macros", @@ -3505,7 +3607,7 @@ dependencies = [ "slip10_ed25519", "smooth-operator", "thiserror", - "tiny-bip39 0.8.2 (git+https://github.com/anoma/tiny-bip39.git?rev=bf0f6d8713589b83af7a917366ec31f5275c0e57)", + "tiny-bip39 0.8.2", "tiny-hderive", "toml 0.5.11", "zeroize", @@ -3865,6 +3967,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -3913,6 +4037,16 @@ dependencies = [ "crypto-mac", ] +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest 0.10.7", + "password-hash 0.3.2", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -3930,7 +4064,7 @@ checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", "hmac 0.12.1", - "password-hash", + "password-hash 0.5.0", ] [[package]] @@ -4065,6 +4199,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.7.0" @@ -5100,9 +5246,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "subtle-encoding" @@ -5426,8 +5572,7 @@ dependencies = [ [[package]] name = "tiny-bip39" version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +source = "git+https://github.com/anoma/tiny-bip39.git?rev=bf0f6d8713589b83af7a917366ec31f5275c0e57#bf0f6d8713589b83af7a917366ec31f5275c0e57" dependencies = [ "anyhow", "hmac 0.8.1", @@ -5444,16 +5589,16 @@ dependencies = [ [[package]] name = "tiny-bip39" -version = "0.8.2" -source = "git+https://github.com/anoma/tiny-bip39.git?rev=bf0f6d8713589b83af7a917366ec31f5275c0e57#bf0f6d8713589b83af7a917366ec31f5275c0e57" +version = "1.0.0" +source = "git+https://github.com/anoma/tiny-bip39?rev=743d537349c8deab14409ce726b868dcde90fd8e#743d537349c8deab14409ce726b868dcde90fd8e" dependencies = [ "anyhow", - "hmac 0.8.1", + "hmac 0.12.1", "once_cell", - "pbkdf2 0.4.0", - "rand 0.7.3", + "pbkdf2 0.11.0", + "rand 0.8.5", "rustc-hash", - "sha2 0.9.9", + "sha2 0.10.8", "thiserror", "unicode-normalization", "wasm-bindgen", diff --git a/packages/sdk/lib/Cargo.toml b/packages/sdk/lib/Cargo.toml index 6d140cfd2c..bb3c90a1d9 100644 --- a/packages/sdk/lib/Cargo.toml +++ b/packages/sdk/lib/Cargo.toml @@ -22,7 +22,8 @@ namada_tx = { git = "https://github.com/anoma/namada", rev = "49a4a5d3260423df19 [dependencies] async-trait = {version = "0.1.51"} -tiny-bip39 = "0.8.2" +# tiny-bip39 = "0.8.2" +tiny-bip39 = { git = "https://github.com/anoma/tiny-bip39", rev = "743d537349c8deab14409ce726b868dcde90fd8e" } chrono = "0.4.22" getrandom = { version = "0.2.7", features = ["js"] } gloo-utils = { version = "0.1.5", features = ["serde"] } @@ -44,6 +45,12 @@ zeroize = "1.6.0" hex = "0.4.3" reqwest = "0.11.25" subtle-encoding = "0.5.1" +aes-gcm = "0.10.1" +argon2 = "0.4.1" +slip10_ed25519 = "0.1.3" +password-hash = "0.3.2" +masp_primitives = { git = "https://github.com/anoma/masp", tag = "v1.1.0" } +borsh-ext = { git = "https://github.com/heliaxdev/borsh-ext", tag = "v1.2.0" } [dependencies.web-sys] version = "0.3.4" diff --git a/packages/sdk/lib/src/crypto/aes.rs b/packages/sdk/lib/src/crypto/aes.rs new file mode 100644 index 0000000000..05c2f2a033 --- /dev/null +++ b/packages/sdk/lib/src/crypto/aes.rs @@ -0,0 +1,99 @@ +use crate::crypto::pointer_types::VecU8Pointer; +use aes_gcm::{ + aead::{generic_array::GenericArray, Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use thiserror::Error; +use wasm_bindgen::prelude::*; +use zeroize::Zeroize; + +#[derive(Debug, Error)] +pub enum AESError { + #[error("Invalid key size! Minimum key size is 32.")] + KeyLengthError, + #[error("Invalid IV! Expected 96 bits (12 bytes)")] + IVSizeError, +} + +#[wasm_bindgen] +pub struct AES { + cipher: Aes256Gcm, + iv: [u8; 12], +} + +#[wasm_bindgen] +impl AES { + #[wasm_bindgen(constructor)] + pub fn new(key: VecU8Pointer, iv: Vec) -> Result { + if key.length < 32 { + return Err(format!( + "{} Received {}", + AESError::KeyLengthError, + key.length + )); + } + let mut key = GenericArray::from_iter(key.vec.clone().into_iter()); + let iv: [u8; 12] = match iv.try_into() { + Ok(iv) => iv, + Err(_) => { + key.zeroize(); + return Err(AESError::IVSizeError.to_string()); + } + }; + + let aes = AES { + cipher: Aes256Gcm::new(&key), + iv, + }; + key.zeroize(); + Ok(aes) + } + + pub fn encrypt(&self, mut text: String) -> Result, String> { + let nonce = Nonce::from_slice(&self.iv); + let result = self + .cipher + .encrypt(nonce, text.as_ref()) + .map_err(|err| err.to_string()); + text.zeroize(); + result + } + + pub fn decrypt(&self, ciphertext: Vec) -> Result { + let nonce = Nonce::from_slice(&self.iv); + let plaintext = self + .cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|err| err.to_string())?; + + Ok(VecU8Pointer::new(plaintext)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::rng::{ByteSize, Rng}; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_encrypt_and_decrypt() { + let key = Rng::generate_bytes(Some(ByteSize::N32)) + .expect("Generating random bytes should not fail"); + let iv = Rng::generate_bytes(Some(ByteSize::N12)) + .expect("Generating random bytes should not fail"); + let aes = AES::new(VecU8Pointer::new(key), iv).unwrap(); + let plaintext = "my secret message"; + let encrypted = aes + .encrypt(String::from(plaintext)) + .expect("AES should not fail encrypting plaintext"); + + let decrypted: &[u8] = &aes + .decrypt(encrypted) + .expect("AES should not fail decrypting ciphertext") + .vec; + let decrypted = std::str::from_utf8(decrypted).expect("Should parse as string"); + + assert_eq!(decrypted, plaintext); + } +} diff --git a/packages/sdk/lib/src/crypto/argon2.rs b/packages/sdk/lib/src/crypto/argon2.rs new file mode 100644 index 0000000000..98cf0b4c4b --- /dev/null +++ b/packages/sdk/lib/src/crypto/argon2.rs @@ -0,0 +1,214 @@ +use crate::crypto::pointer_types::VecU8Pointer; +use password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use wasm_bindgen::prelude::*; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[wasm_bindgen] +pub struct Argon2Params { + m_cost: u32, + t_cost: u32, + p_cost: u32, +} + +#[wasm_bindgen] +impl Argon2Params { + #[wasm_bindgen(constructor)] + pub fn new(m_cost: u32, t_cost: u32, p_cost: u32) -> Self { + Self { + m_cost, + t_cost, + p_cost, + } + } + + #[wasm_bindgen(getter)] + pub fn m_cost(&self) -> u32 { + self.m_cost + } + + #[wasm_bindgen(getter)] + pub fn t_cost(&self) -> u32 { + self.t_cost + } + + #[wasm_bindgen(getter)] + pub fn p_cost(&self) -> u32 { + self.p_cost + } +} + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct Argon2 { + #[zeroize(skip)] + salt: SaltString, + password: Vec, + #[zeroize(skip)] + params: argon2::Params, +} + +/// Argon2 password hashing +#[wasm_bindgen] +impl Argon2 { + #[wasm_bindgen(constructor)] + pub fn new( + password: String, + salt: Option, + params: Option, + ) -> Result { + let password = Vec::from(password.as_bytes()); + let default_params = argon2::Params::default(); + + let salt = match salt { + Some(salt) => SaltString::new(&salt).map_err(|err| err.to_string())?, + None => SaltString::generate(&mut OsRng), + }; + + let params = match params { + Some(params) => argon2::Params::new(params.m_cost, params.t_cost, params.p_cost, None) + .map_err(|err| err.to_string())?, + None => default_params, + }; + + Ok(Argon2 { + salt, + password, + params, + }) + } + + pub fn to_hash(&self) -> Result { + let argon2 = argon2::Argon2::default(); + let bytes: &[u8] = &self.password; + let params = &self.params; + + // Hash password to PHC string ($argon2id$v=19$...) + let password_hash = argon2 + .hash_password_customized( + bytes, + None, // Default alg_id = Argon2id + None, // Default ver = v19 + params.to_owned(), + &self.salt, + ) + .map_err(|err| err.to_string())? + .to_string(); + + Ok(password_hash) + } + + pub fn verify(&self, hash: String) -> Result<(), String> { + let argon2 = argon2::Argon2::default(); + let bytes: &[u8] = &self.password; + let parsed_hash = PasswordHash::new(&hash).map_err(|err| err.to_string())?; + + match argon2.verify_password(bytes, &parsed_hash) { + Ok(_) => Ok(()), + Err(err) => Err(err.to_string()), + } + } + + pub fn params(&self) -> Argon2Params { + Argon2Params::new( + self.params.m_cost(), + self.params.t_cost(), + self.params.p_cost(), + ) + } + + /// Convert PHC string to serialized key + pub fn key(&self) -> Result { + let mut hash = self.to_hash()?; + let split = hash.split('$'); + let items: Vec<&str> = split.collect(); + + let key = items[items.len() - 1]; + let vec = Vec::from(key.as_bytes()); + hash.zeroize(); + + Ok(VecU8Pointer::new(vec)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_hash_password() { + let password = "unhackable"; + let argon2 = Argon2::new(password.into(), None, None) + .expect("Creating instance with default params should not fail"); + let hash = argon2 + .to_hash() + .expect("Hashing password with Argon2 should not fail!"); + + assert!(argon2.verify(hash).is_ok()); + } + + #[wasm_bindgen_test] + fn can_hash_password_with_custom_params() { + // Memory cost + let m_cost = 2048; + // Iterations/Time cost: + let t_cost = 2; + // Degree of parallelism: + let p_cost = 2; + let params = Argon2Params::new(m_cost, t_cost, p_cost); + let password = "unhackable"; + let argon2 = Argon2::new(password.into(), None, Some(params)) + .expect("Creating instance with custom params should not fail"); + + let hash = argon2 + .to_hash() + .expect("Hashing password with Argon2 should not fail!"); + assert!(argon2.verify(hash).is_ok()); + } + + #[wasm_bindgen_test] + fn can_verify_stored_hash() { + let password = "unhackable"; + let argon2 = Argon2::new(password.into(), None, None) + .expect("Creating instance with default params should not fail"); + let stored_hash = "$argon2id$v=19$m=4096,t=3,p=1$0UUjc4ZBOJJLTPrS1mQr1w$orbgGGRzWC0GvplgJuteaDORldnQiJfVumhXSuwO3UE"; + + // With randomly generated salt, this should not create + // an equivalent hash: + assert_ne!(argon2.to_hash().unwrap(), stored_hash); + assert!(argon2.verify(stored_hash.to_string()).is_ok()); + } + + #[wasm_bindgen_test] + fn can_verify_stored_hash_with_custom_salt() { + let password = "unhackable"; + let salt = String::from("41oVKhMIBZ+oF4efwq7e0A"); + let argon2 = Argon2::new(password.into(), Some(salt), None) + .expect("Creating instance with default params should not fail"); + let stored_hash = "$argon2id$v=19$m=4096,t=3,p=1$41oVKhMIBZ+oF4efwq7e0A$ec9kY153e/S6z9awayWdUTLdaQowoAxrdo7ZkTjhBl4"; + + // Providing salt, this should create an equivalent hash: + assert_eq!(argon2.to_hash().unwrap(), stored_hash); + assert!(argon2.verify(stored_hash.to_string()).is_ok()); + } + + #[wasm_bindgen_test] + fn can_get_key_and_params() { + let password = "unhackable"; + let argon2 = Argon2::new(password.into(), None, None) + .expect("Creating instance with default params should not fail"); + let hash = argon2 + .to_hash() + .expect("Hashing password with Argon2 should not fail!"); + + assert!(argon2.verify(hash).is_ok()); + + let params = argon2.params(); + let key = argon2.key().expect("Creating key should not fail"); + + assert_eq!(params.m_cost(), 4096); + assert_eq!(params.t_cost(), 3); + assert_eq!(params.p_cost(), 1); + assert_eq!(key.vec.len(), 43); + } +} diff --git a/packages/sdk/lib/src/crypto/bip32.rs b/packages/sdk/lib/src/crypto/bip32.rs new file mode 100644 index 0000000000..7cd4e5ddd6 --- /dev/null +++ b/packages/sdk/lib/src/crypto/bip32.rs @@ -0,0 +1,137 @@ +use crate::crypto::pointer_types::{StringPointer, VecU8Pointer}; +use rand::{rngs::OsRng, RngCore}; +use slip10_ed25519; +use thiserror::Error; +use wasm_bindgen::prelude::*; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, Error)] +pub enum HDWalletError { + #[error("Unable to derive keys from path")] + DerivationError, + #[error("Invalid key size")] + InvalidKeySize, + #[error("Invalid seed")] + InvalidSeed, +} + +#[wasm_bindgen] +#[derive(Zeroize)] +pub struct Key { + bytes: [u8; 32], +} + +/// A 32 byte ed25519 key +#[wasm_bindgen] +impl Key { + #[wasm_bindgen(constructor)] + pub fn new(bytes: Vec) -> Result { + let bytes: [u8; 32] = match bytes.try_into() { + Ok(bytes) => bytes, + Err(err) => return Err(format!("{}: {:?}", HDWalletError::InvalidKeySize, err)), + }; + + Ok(Key { bytes }) + } + + pub fn to_bytes(&self) -> Vec { + Vec::from(self.bytes) + } + + pub fn to_hex(&self) -> StringPointer { + let bytes: &[u8] = &self.bytes; + let string = hex::encode(&bytes); + StringPointer::new(string) + } +} + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct HDWallet { + seed: [u8; 64], +} + +/// A set of methods to derive keys from a BIP32/BIP44 path +#[wasm_bindgen] +impl HDWallet { + #[wasm_bindgen(constructor)] + pub fn new(seed_ptr: VecU8Pointer) -> Result { + let seed: [u8; 64] = match seed_ptr.vec.clone().try_into() { + Ok(seed) => seed, + Err(err) => return Err(format!("{}: {:?}", HDWalletError::InvalidSeed, err)), + }; + + Ok(HDWallet { seed }) + } + + pub fn from_seed(seed: Vec) -> Result { + let seed: [u8; 64] = match seed.try_into() { + Ok(seed) => seed, + Err(err) => return Err(format!("{}: {:?}", HDWalletError::InvalidSeed, err)), + }; + + Ok(HDWallet { seed }) + } + + /// Derive account from a seed and a path + pub fn derive(&self, path: Vec) -> Result { + let key = slip10_ed25519::derive_ed25519_private_key(&self.seed, &path); + let private = Key::new(Vec::from(key)) + .map_err(|err| format!("{}: {:?}", HDWalletError::DerivationError, err))?; + + Ok(private) + } + + pub fn disposable_keypair() -> Result { + let path = vec![44, 877, 0, 0, 0]; + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + + let key = slip10_ed25519::derive_ed25519_private_key(&key, &path); + + Key::new(Vec::from(key)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::bip39::Mnemonic; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_derive_keys_from_path() { + let phrase = "caught pig embody hip goose like become worry face oval manual flame \ + pizza steel viable proud eternal speed chapter sunny boat because view bullet"; + let mnemonic = + Mnemonic::from_phrase(phrase.into()).expect("Should not fail with a valid phrase!"); + let seed = mnemonic.to_seed(None).unwrap(); + let bip44: HDWallet = HDWallet::new(seed).unwrap(); + let path = vec![44, 877, 0, 0, 0]; + + let key = bip44.derive(path).expect("Should derive keys from a path"); + + assert_eq!( + key.to_bytes(), + [ + 228, 104, 14, 30, 58, 200, 239, 116, 140, 154, 151, 251, 162, 132, 183, 188, 107, + 0, 45, 182, 36, 48, 46, 39, 113, 29, 252, 73, 44, 242, 125, 30 + ] + ); + } + + // TODO: we use test instead of wasm_bindgen_test because we want to catch the panic + #[wasm_bindgen_test] + fn invalid_seed_should_panic() { + let res = HDWallet::new(VecU8Pointer::new(vec![0, 1, 2, 3, 4])); + + assert!(res.is_err()); + } + + #[wasm_bindgen_test] + fn invalid_key_should_panic() { + let res = Key::new(vec![0, 1, 2, 3, 4]); + + assert!(res.is_err()); + } +} diff --git a/packages/sdk/lib/src/crypto/bip39.rs b/packages/sdk/lib/src/crypto/bip39.rs new file mode 100644 index 0000000000..168738c92b --- /dev/null +++ b/packages/sdk/lib/src/crypto/bip39.rs @@ -0,0 +1,158 @@ +use crate::crypto::pointer_types::{ + new_vec_string_pointer, StringPointer, VecStringPointer, VecU8Pointer, +}; +use bip39::{Language, Mnemonic as M, MnemonicType, Seed}; +use thiserror::Error; +use wasm_bindgen::prelude::*; +use zeroize::Zeroize; + +#[derive(Debug, Error)] +pub enum Bip39Error { + #[error("Invalid phrase")] + InvalidPhrase, +} + +#[wasm_bindgen] +#[derive(Copy, Clone)] +pub enum PhraseSize { + N12 = 12, + N24 = 24, +} + +#[wasm_bindgen] +pub struct Mnemonic { + mnemonic: M, +} + +#[wasm_bindgen] +impl Mnemonic { + #[wasm_bindgen(constructor)] + pub fn new(size: PhraseSize) -> Mnemonic { + let mnemonic_type = match size { + PhraseSize::N12 => MnemonicType::Words12, + PhraseSize::N24 => MnemonicType::Words24, + }; + + let mnemonic = M::new(mnemonic_type, Language::English); + + Mnemonic { mnemonic } + } + + pub fn validate(phrase: &str) -> bool { + M::validate(phrase, Language::English).is_ok() + } + + pub fn from_phrase(mut phrase: String) -> Result { + // First validate phrase, provide error to client if this fails + M::validate(&phrase, Language::English).map_err(|e| format!("{}", e))?; + + let mnemonic = M::from_phrase(&phrase, Language::English) + .map_err(|e| format!("{}: {:?}", Bip39Error::InvalidPhrase, e))?; + + phrase.zeroize(); + + Ok(Mnemonic { mnemonic }) + } + + pub fn to_seed(&self, passphrase: Option) -> Result { + let mut passphrase = match passphrase { + Some(passphrase) => passphrase.string.clone(), + None => "".into(), + }; + + let seed = Seed::new(&self.mnemonic, &passphrase); + passphrase.zeroize(); + + Ok(VecU8Pointer::new(Vec::from(seed.as_bytes()))) + } + + pub fn to_words(&self) -> Result { + let words: Vec = self + .mnemonic + .phrase() + .split(' ') + .map(String::from) + .collect(); + Ok(new_vec_string_pointer(words)) + } + + pub fn phrase(&self) -> String { + String::from(self.mnemonic.phrase()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_generate_mnemonic_from_size() { + let mnemonic = Mnemonic::new(PhraseSize::N12); + let phrase = mnemonic.phrase(); + let words: Vec<&str> = phrase.split(' ').collect(); + + assert_eq!(words.iter().len(), 12); + + let mnemonic = Mnemonic::new(PhraseSize::N24); + let phrase = mnemonic.phrase(); + let words: Vec<&str> = phrase.split(' ').collect(); + + assert_eq!(words.iter().len(), 24); + } + + #[wasm_bindgen_test] + fn can_generate_seed_from_phrase() { + let phrase = "caught pig embody hip goose like become worry face oval manual flame \ + pizza steel viable proud eternal speed chapter sunny boat because view bullet"; + let mnemonic = Mnemonic::from_phrase(phrase.into()).unwrap(); + let seed = mnemonic + .to_seed(None) + .expect("Should return seed from mnemonic phrase"); + + assert_eq!(seed.vec.len(), 64); + } + + #[wasm_bindgen_test] + fn can_restore_seed_from_phrase() { + let phrase = "caught pig embody hip goose like become worry face oval manual flame \ + pizza steel viable proud eternal speed chapter sunny boat because view bullet"; + let seed_bytes = vec![ + 178, 64, 160, 168, 33, 68, 84, 63, 0, 137, 121, 29, 66, 47, 123, 36, 64, 38, 160, 236, + 93, 38, 53, 157, 169, 119, 42, 153, 188, 80, 209, 149, 51, 92, 251, 168, 150, 220, 70, + 78, 230, 16, 152, 160, 85, 248, 115, 82, 183, 126, 96, 112, 58, 238, 230, 63, 89, 239, + 0, 250, 163, 169, 166, 174, + ]; + let mnemonic = Mnemonic::from_phrase(phrase.into()).unwrap(); + let seed = mnemonic + .to_seed(None) + .expect("Should return seed from mnemonic phrase"); + + assert_eq!(seed.vec, seed_bytes); + } + + #[wasm_bindgen_test] + fn invalid_phrase_should_panic() { + let bad_phrase = "caught pig embody hip goose like become"; + let res = Mnemonic::from_phrase(bad_phrase.into()); + + assert!(res.is_err()); + } + + #[wasm_bindgen_test] + fn can_generate_word_list_from_mnemonic() { + let mnemonic = Mnemonic::new(PhraseSize::N12); + let words = mnemonic + .to_words() + .expect("Should return a VecStringPointer containing the words"); + + assert_eq!(words.strings.len(), 12); + + let mnemonic = Mnemonic::new(PhraseSize::N24); + let words = mnemonic + .to_words() + .expect("Should return a VecStringPointer containing the words"); + + assert_eq!(words.strings.len(), 24); + } +} diff --git a/packages/sdk/lib/src/crypto/mod.rs b/packages/sdk/lib/src/crypto/mod.rs new file mode 100644 index 0000000000..603532b31b --- /dev/null +++ b/packages/sdk/lib/src/crypto/mod.rs @@ -0,0 +1,8 @@ +pub mod aes; +// pub mod argon2; +pub mod bip32; +pub mod bip39; +pub mod pointer_types; +pub mod rng; +// pub mod salt; +// pub mod zip32; diff --git a/packages/sdk/lib/src/crypto/pointer_types.rs b/packages/sdk/lib/src/crypto/pointer_types.rs new file mode 100644 index 0000000000..2d556a7a1b --- /dev/null +++ b/packages/sdk/lib/src/crypto/pointer_types.rs @@ -0,0 +1,98 @@ +//! Types for wrapping sensitive data. +//! +//! Wrapped values will not automatically be accessible to JavaScript, so we can +//! avoid loading sensitive data into browser memory. However, values can be +//! accessed in JavaScript by using the pointer and length fields to read +//! directly from WASM memory if required. +//! +//! These types will also zeroize sensitive data when dropped. +use wasm_bindgen::prelude::*; +use zeroize::ZeroizeOnDrop; + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct VecU8Pointer { + #[zeroize(skip)] + pub pointer: *const u8, + #[zeroize(skip)] + pub length: usize, + #[wasm_bindgen(skip)] + pub vec: Vec, +} + +#[wasm_bindgen] +impl VecU8Pointer { + #[wasm_bindgen(constructor)] + pub fn new(vec: Vec) -> VecU8Pointer { + VecU8Pointer { + pointer: vec.as_ptr(), + length: vec.len(), + vec, + } + } + + pub fn clone(&self) -> VecU8Pointer { + VecU8Pointer::new(self.vec.clone()) + } +} + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct StringPointer { + #[zeroize(skip)] + pub pointer: *const u8, + #[zeroize(skip)] + pub length: usize, + #[wasm_bindgen(skip)] + pub string: String, +} + +#[wasm_bindgen] +impl StringPointer { + #[wasm_bindgen(constructor)] + pub fn new(string: String) -> StringPointer { + StringPointer { + pointer: string.as_ptr(), + length: string.len(), + string, + } + } + + pub fn clone(&self) -> StringPointer { + StringPointer::new(self.string.clone()) + } +} + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct VecStringPointer { + #[zeroize(skip)] + pointers: Vec, + #[zeroize(skip)] + lengths: Vec, + #[wasm_bindgen(skip)] + pub strings: Vec, +} + +#[wasm_bindgen] +impl VecStringPointer { + #[wasm_bindgen(getter)] + pub fn pointers(&self) -> Vec { + self.pointers.clone() + } + + #[wasm_bindgen(getter)] + pub fn lengths(&self) -> Vec { + self.lengths.clone() + } +} + +pub fn new_vec_string_pointer(strings: Vec) -> VecStringPointer { + let pointers = strings.iter().map(|str| str.as_ptr() as usize).collect(); + let lengths = strings.iter().map(|str| str.len()).collect(); + VecStringPointer { + pointers, + lengths, + strings, + } +} diff --git a/packages/sdk/lib/src/crypto/rng.rs b/packages/sdk/lib/src/crypto/rng.rs new file mode 100644 index 0000000000..775a139b44 --- /dev/null +++ b/packages/sdk/lib/src/crypto/rng.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Serialize, Deserialize)] +pub enum ByteSize { + N12 = 12, + N24 = 24, + N32 = 32, +} + +#[wasm_bindgen] +pub struct Rng; + +#[wasm_bindgen] +impl Rng { + pub fn generate_bytes(size: Option) -> Result, String> { + let size = match size { + Some(ByteSize::N12) => 12, + Some(ByteSize::N24) => 24, + Some(ByteSize::N32) => 32, + None => 32, + }; + + let mut buf = [0u8; 32]; + getrandom::getrandom(&mut buf).map_err(|err| err.to_string())?; + + let buf = Vec::from(buf); + + Ok(Vec::from(&buf[0..size])) + } +} + +#[cfg(test)] +mod test { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_generate_bytes() { + let bytes = + Rng::generate_bytes(Some(ByteSize::N12)).expect("Generating 12 bytes should not fail"); + + assert_eq!(bytes.len(), 12); + + let bytes = + Rng::generate_bytes(Some(ByteSize::N24)).expect("Generating 24 bytes should not fail"); + + assert_eq!(bytes.len(), 24); + + let bytes = + Rng::generate_bytes(Some(ByteSize::N32)).expect("Generating 32 bytes should not fail"); + + assert_eq!(bytes.len(), 32); + } +} diff --git a/packages/sdk/lib/src/crypto/salt.rs b/packages/sdk/lib/src/crypto/salt.rs new file mode 100644 index 0000000000..1ddd4b674c --- /dev/null +++ b/packages/sdk/lib/src/crypto/salt.rs @@ -0,0 +1,61 @@ +use password_hash::{rand_core::OsRng, SaltString}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Salt { + salt: SaltString, +} + +#[wasm_bindgen] +impl Salt { + #[wasm_bindgen(constructor)] + pub fn new(salt: String) -> Result { + let salt = SaltString::new(&salt).map_err(|err| err.to_string())?; + + Ok(Salt { salt }) + } + + pub fn generate() -> Self { + Self { + salt: SaltString::generate(&mut OsRng), + } + } + + pub fn to_bytes(&self) -> Result, String> { + let salt_string = &self.salt.to_string(); + let salt = argon2::password_hash::Salt::new(salt_string).map_err(|err| err.to_string())?; + let bytes: &[u8] = salt.as_bytes(); + Ok(Vec::from(bytes)) + } + + pub fn as_string(&self) -> String { + self.salt.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_generate_salt_from_string() { + let salt_string = String::from("41oVKhMIBZ+oF4efwq7e0A"); + let salt = + Salt::new(salt_string.clone()).expect("Creating instance of Salt should not fail!"); + + assert_eq!(salt_string, salt.as_string()); + } + + #[wasm_bindgen_test] + fn can_generate_salt_bytes_from_string() { + let salt = String::from("41oVKhMIBZ+oF4efwq7e0A"); + let salt = Salt::new(salt).expect("Creating salt from string should not fail"); + let expected_bytes = vec![ + 52, 49, 111, 86, 75, 104, 77, 73, 66, 90, 43, 111, 70, 52, 101, 102, 119, 113, 55, 101, + 48, 65, + ]; + let bytes = salt.to_bytes().expect("Returning to bytes should not fail"); + assert_eq!(bytes, expected_bytes); + } +} diff --git a/packages/sdk/lib/src/crypto/zip32.rs b/packages/sdk/lib/src/crypto/zip32.rs new file mode 100644 index 0000000000..b0be45872b --- /dev/null +++ b/packages/sdk/lib/src/crypto/zip32.rs @@ -0,0 +1,185 @@ +//! ShieldedHDWallet - Provide wasm_bindgen bindings for zip32 HD wallets +//! Imports from masp_primitives::zip32, instead of zcash_primitives::zip32, as +//! the value for constant ZIP32_SAPLING_MASTER_PERSONALIZATION is different! +//! Otherwise, these implementations should be equivalent. +use borsh_ext::BorshSerializeExt; +use masp_primitives::{ + sapling::PaymentAddress, + zip32::{sapling, ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey}, +}; +use namada_sdk::borsh::BorshDeserialize; +use wasm_bindgen::prelude::*; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[wasm_bindgen] +#[derive(Zeroize)] +pub struct DerivationResult { + xsk: Vec, + xfvk: Vec, + payment_address: Vec, +} + +#[wasm_bindgen] +impl DerivationResult { + pub fn xsk(&self) -> Vec { + self.xsk.clone() + } + + pub fn xfvk(&self) -> Vec { + self.xfvk.clone() + } + + pub fn payment_address(&self) -> Vec { + self.payment_address.clone() + } +} + +#[wasm_bindgen] +#[derive(ZeroizeOnDrop)] +pub struct ShieldedHDWallet { + seed: [u8; 32], +} + +#[wasm_bindgen] +impl ShieldedHDWallet { + #[wasm_bindgen(constructor)] + pub fn new(seed: JsValue, path: Vec) -> Result { + let seed = js_sys::Uint8Array::from(seed).to_vec(); + let sk = slip10_ed25519::derive_ed25519_private_key(&seed, &path); + + Ok(ShieldedHDWallet { seed: sk }) + } + + pub fn new_from_sk(sk_bytes: Vec) -> Result { + let sk: [u8; 32] = match sk_bytes.try_into() { + Ok(bytes) => bytes, + Err(err) => return Err(format!("Invalid Private Key! {:?}", err)), + }; + + Ok(ShieldedHDWallet { seed: sk }) + } + + pub fn derive( + &self, + path: Vec, + diversifier: Option>, + ) -> Result { + let master_spend_key = sapling::ExtendedSpendingKey::master(&self.seed); + + let purpose = path.first().expect("zip32 purpose is required!"); + let coin_type = path.get(1).expect("zip32 coin_type is required!"); + let account = path.get(2).expect("zip32 account is required!"); + + // Optional address index + let address_index = path.get(3); + + let mut zip32_path: Vec = vec![purpose, coin_type, account] + .iter() + .map(|i| ChildIndex::Hardened(**i)) + .collect(); + + if address_index.is_some() { + zip32_path.push(ChildIndex::NonHardened(*address_index.unwrap())); + } + + let xsk: ExtendedSpendingKey = + ExtendedSpendingKey::from_path(&master_spend_key, &zip32_path); + + let xfvk = ExtendedFullViewingKey::from(&xsk); + + // We either use passed diversifier or the default payment_address + let payment_address: PaymentAddress = match diversifier { + Some(d) => { + let diversifier = BorshDeserialize::try_from_slice(&d).unwrap(); + xfvk.fvk.vk.to_payment_address(diversifier).unwrap() + } + None => xfvk.default_address().1, + }; + + Ok(DerivationResult { + xsk: xsk.serialize_to_vec(), + xfvk: xfvk.serialize_to_vec(), + payment_address: payment_address.serialize_to_vec(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::bip39; + use masp_primitives::sapling::PaymentAddress; + use wasm_bindgen_test::*; + + const KEY_SIZE: usize = 96; + + #[wasm_bindgen_test] + fn can_instantiate_from_seed() { + let seed = JsValue::from(js_sys::Uint8Array::new_with_length(64)); + let path = vec![44, 877, 0, 0, 0]; + let shielded_wallet = ShieldedHDWallet::new(seed, path); + + assert!(shielded_wallet.is_ok()); + } + + #[wasm_bindgen_test] + fn can_derive_shielded_key_to_serialized() { + let seed = JsValue::from(js_sys::Uint8Array::new_with_length(64)); + let path = vec![44, 877, 0, 0, 0]; + let shielded_wallet = ShieldedHDWallet::new(seed, path) + .expect("Instantiating ShieldedHDWallet should not fail"); + + let DerivationResult { + ref payment_address, + ref xsk, + ref xfvk, + } = shielded_wallet + .derive(vec![32, 877, 0], None) + .expect("Deriving from ExtendedKeys should not fail"); + + let payment_address: PaymentAddress = + borsh::BorshDeserialize::try_from_slice(payment_address) + .expect("Should be able to deserialize payment address!"); + let xsk: ExtendedSpendingKey = borsh::BorshDeserialize::try_from_slice(xsk) + .expect("Should be able to deserialize extended spending key!"); + let xfvk: ExtendedFullViewingKey = borsh::BorshDeserialize::try_from_slice(xfvk) + .expect("Should be able to deserialize full viewing key!"); + + assert_eq!(payment_address.to_bytes().len(), 43); + assert_eq!(xsk.expsk.to_bytes().len(), KEY_SIZE); + assert_eq!(xfvk.fvk.to_bytes().len(), KEY_SIZE); + } + + #[wasm_bindgen_test] + fn can_restore_shielded_keys_from_mnemonic() { + let phrase = "great sphere inmate december menu warrior adjust glass flat heavy act mail"; + let mnemonic = bip39::Mnemonic::from_phrase(phrase.into()).unwrap(); + let seed = mnemonic + .to_seed(None) + .expect("Should return seed from mnemonic phrase"); + let path = vec![44, 877, 0, 0, 0]; + + let shielded_wallet = ShieldedHDWallet::new(JsValue::from(seed), path) + .expect("Instantiating ShieldedHDWallet should not fail"); + + let shielded_account = shielded_wallet + .derive(vec![32, 877, 0], None) + .expect("Deriving from ExtendedKeys should not fail"); + + let payment_address = PaymentAddress::try_from_slice(&shielded_account.payment_address()) + .expect("should instantiate from serialized bytes"); + let xfvk = ExtendedFullViewingKey::try_from_slice(&shielded_account.xfvk()) + .expect("should instantiate from serialized bytes"); + + assert_eq!(payment_address.to_string(), "efad0a092281f049a04250b91b84a8454cec0c5da75821ef7fd2deb684201cc83dd7bb287c241b11cd88d9"); + assert_eq!( + xfvk.fvk.to_string(), + format!( + "{}{}{}", + "a654d32c7b361f77a774a3f80c7dcd053a9e904f0c3bab1e9e207ed4e01434103fa", + "d5db7d3784841e0dd5f1b931b515186da3058562c103eaf11dc665c9da19f12ea71", + "19818ed1f124bd0573f15a82e97893664b7bc3e80b19ed96ba4f52eef3", + ) + ); + } +} diff --git a/packages/sdk/lib/src/lib.rs b/packages/sdk/lib/src/lib.rs index 1925781c72..83d15cb37e 100644 --- a/packages/sdk/lib/src/lib.rs +++ b/packages/sdk/lib/src/lib.rs @@ -2,6 +2,7 @@ //! //! A library of functions to integrate shared functionality from the Namada ecosystem +pub mod crypto; pub mod query; pub mod rpc_client; pub mod sdk; diff --git a/packages/sdk/lib/src/rpc_client.js b/packages/sdk/lib/src/rpc_client.js deleted file mode 100644 index 0fb9489df4..0000000000 --- a/packages/sdk/lib/src/rpc_client.js +++ /dev/null @@ -1,57 +0,0 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/** - * Small wrapper for fetch to make it easier to pass props - * Called wasmFetch to avoid naming conflict - */ -export function wasmFetch(url, method, body) { - return __awaiter(this, void 0, void 0, function () { - var res; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, fetch(url, { - method: method, - body: body, - })]; - case 1: - res = _a.sent(); - return [2 /*return*/, res]; - } - }); - }); -} diff --git a/packages/sdk/lib/src/sdk/masp/masp.node.js b/packages/sdk/lib/src/sdk/masp/masp.node.js deleted file mode 100644 index f46e969f5f..0000000000 --- a/packages/sdk/lib/src/sdk/masp/masp.node.js +++ /dev/null @@ -1,30 +0,0 @@ -const fs = require("node:fs"); - -function writeFileSync(path, ui8a) { - fs.writeFileSync(path, Buffer.from(ui8a)); -} - -function readFileSync(path) { - const buffer = fs.readFileSync(path).buffer; - return buffer; -} - -function renameSync(pathA, pathB) { - fs.renameSync(pathA, pathB); -} - -function unlinkSync(path) { - fs.unlinkSync(path); -} - -function existsSync(path) { - return fs.existsSync(path); -} - -module.exports = { - writeFileSync, - readFileSync, - renameSync, - unlinkSync, - existsSync, -}; diff --git a/packages/sdk/lib/src/sdk/mod.js b/packages/sdk/lib/src/sdk/mod.js deleted file mode 100644 index abad41955d..0000000000 --- a/packages/sdk/lib/src/sdk/mod.js +++ /dev/null @@ -1,292 +0,0 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var _a; -var PREFIX = "Namada::SDK"; -var MASP_MPC_RELEASE_URL = "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/"; -var sha256Hash = function (msg) { return __awaiter(void 0, void 0, void 0, function () { - var hashBuffer, hashArray; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, crypto.subtle.digest("SHA-256", msg)]; - case 1: - hashBuffer = _a.sent(); - hashArray = Array.from(new Uint8Array(hashBuffer)); - // Return hash as hex - return [2 /*return*/, hashArray.map(function (byte) { return byte.toString(16).padStart(2, "0"); }).join("")]; - } - }); -}); }; -var MaspParam; -(function (MaspParam) { - MaspParam["Output"] = "masp-output.params"; - MaspParam["Convert"] = "masp-convert.params"; - MaspParam["Spend"] = "masp-spend.params"; -})(MaspParam || (MaspParam = {})); -/** - * The following sha256 digests where produced by downloading the following: - * https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-convert.params - * https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-spend.params - * https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-output.params - * - * And running "sha256sum" against each file: - * - * > sha256sum masp-convert.params - * 8e049c905e0e46f27662c7577a4e3480c0047ee1171f7f6d9c5b0de757bf71f1 masp-convert.params - * - * > sha256sum masp-spend.params - * 62b3c60ca54bd99eb390198e949660624612f7db7942db84595fa9f1b4a29fd8 masp-spend.params - * - * > sha256sum masp-output.params - * ed8b5d354017d808cfaf7b31eca5c511936e65ef6d276770251f5234ec5328b8 masp-output.params - * - * Length is specified in bytes, and can be retrieved with: - * - * > wc -c < masp-convert.params - * 22570940 - * > wc -c < masp-spend.params - * 49848572 - * > wc -c < masp-output.params - * 16398620 - */ -var MASP_PARAM_ATTR = (_a = {}, - _a[MaspParam.Output] = { - length: 16398620, - sha256sum: "ed8b5d354017d808cfaf7b31eca5c511936e65ef6d276770251f5234ec5328b8", - }, - _a[MaspParam.Spend] = { - length: 49848572, - sha256sum: "62b3c60ca54bd99eb390198e949660624612f7db7942db84595fa9f1b4a29fd8", - }, - _a[MaspParam.Convert] = { - length: 22570940, - sha256sum: "8e049c905e0e46f27662c7577a4e3480c0047ee1171f7f6d9c5b0de757bf71f1", - }, - _a); -var validateMaspParamBytes = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { - var _c, length, sha256sum, hash; - var param = _b.param, bytes = _b.bytes; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - _c = MASP_PARAM_ATTR[param], length = _c.length, sha256sum = _c.sha256sum; - // Reject if invalid length (incomplete download or invalid) - console.info("Validating data length for ".concat(param, ", expecting ").concat(length, "...")); - if (length !== bytes.length) { - return [2 /*return*/, Promise.reject("[".concat(param, "]: Invalid data length! Expected ").concat(length, ", received ").concat(bytes.length, "!"))]; - } - // Reject if invalid hash (otherwise invalid data) - console.info("Validating sha256sum for ".concat(param, ", expecting ").concat(sha256sum, "...")); - return [4 /*yield*/, sha256Hash(bytes)]; - case 1: - hash = _d.sent(); - if (hash !== sha256sum) { - return [2 /*return*/, Promise.reject("[".concat(param, "]: Invalid sha256sum! Expected ").concat(sha256sum, ", received ").concat(hash, "!"))]; - } - return [2 /*return*/, bytes]; - } - }); -}); }; -export function hasMaspParams() { - return __awaiter(this, void 0, void 0, function () { - var _a, _b; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: return [4 /*yield*/, has(MaspParam.Spend)]; - case 1: - _b = (_c.sent()); - if (!_b) return [3 /*break*/, 3]; - return [4 /*yield*/, has(MaspParam.Output)]; - case 2: - _b = (_c.sent()); - _c.label = 3; - case 3: - _a = _b; - if (!_a) return [3 /*break*/, 5]; - return [4 /*yield*/, has(MaspParam.Convert)]; - case 4: - _a = (_c.sent()); - _c.label = 5; - case 5: return [2 /*return*/, (_a)]; - } - }); - }); -} -export function fetchAndStoreMaspParams(url) { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - return [2 /*return*/, Promise.all([ - fetchAndStore(MaspParam.Spend, url), - fetchAndStore(MaspParam.Output, url), - fetchAndStore(MaspParam.Convert, url), - ])]; - }); - }); -} -export function getMaspParams() { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - return [2 /*return*/, Promise.all([ - get(MaspParam.Spend), - get(MaspParam.Output), - get(MaspParam.Convert), - ])]; - }); - }); -} -export function fetchAndStore(param, url) { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, fetchParams(param, url) - .then(function (data) { return set(param, data); }) - .catch(function (e) { - return Promise.reject("Encountered errors fetching ".concat(param, ": ").concat(e)); - })]; - case 1: return [2 /*return*/, _a.sent()]; - } - }); - }); -} -export function fetchParams(param_1) { - return __awaiter(this, arguments, void 0, function (param, url) { - if (url === void 0) { url = MASP_MPC_RELEASE_URL; } - return __generator(this, function (_a) { - return [2 /*return*/, fetch("".concat(url).concat(param)) - .then(function (response) { return response.arrayBuffer(); }) - .then(function (ab) { - var bytes = new Uint8Array(ab); - return validateMaspParamBytes({ param: param, bytes: bytes }); - })]; - }); - }); -} -function getDB() { - return new Promise(function (resolve, reject) { - var request = indexedDB.open(PREFIX); - request.onerror = function (event) { - event.stopPropagation(); - reject(event.target); - }; - request.onupgradeneeded = function (event) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - var db = event.target.result; - db.createObjectStore(PREFIX, { keyPath: "key" }); - }; - request.onsuccess = function () { - resolve(request.result); - }; - }); -} -export function get(key) { - return __awaiter(this, void 0, void 0, function () { - var tx, store; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, getDB()]; - case 1: - tx = (_a.sent()).transaction(PREFIX, "readonly"); - store = tx.objectStore(PREFIX); - return [2 /*return*/, new Promise(function (resolve, reject) { - var request = store.get(key); - request.onerror = function (event) { - event.stopPropagation(); - reject(event.target); - }; - request.onsuccess = function () { - if (!request.result) { - resolve(undefined); - } - else { - resolve(request.result.data); - } - }; - })]; - } - }); - }); -} -export function has(key) { - return __awaiter(this, void 0, void 0, function () { - var tx, store; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, getDB()]; - case 1: - tx = (_a.sent()).transaction(PREFIX, "readonly"); - store = tx.objectStore(PREFIX); - return [2 /*return*/, new Promise(function (resolve, reject) { - var request = store.openCursor(key); - request.onerror = function (event) { - event.stopPropagation(); - reject(event.target); - }; - request.onsuccess = function (e) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - var cursor = e.target.result; - resolve(!!cursor); - }; - })]; - } - }); - }); -} -export function set(key, data) { - return __awaiter(this, void 0, void 0, function () { - var tx, store; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, getDB()]; - case 1: - tx = (_a.sent()).transaction(PREFIX, "readwrite"); - store = tx.objectStore(PREFIX); - return [2 /*return*/, new Promise(function (resolve, reject) { - var request = store.put({ - key: key, - data: data, - }); - request.onerror = function (event) { - event.stopPropagation(); - reject(event.target); - }; - request.onsuccess = function () { - resolve(); - }; - })]; - } - }); - }); -}