From e2191d5e4d0f134ee39b81ff1f1b8018c3e41685 Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Wed, 18 Dec 2024 18:12:31 +0000 Subject: [PATCH] feat: begin porting crypto --- packages/sdk/lib/Cargo.lock | 197 ++++++++++++++--- packages/sdk/lib/Cargo.toml | 9 +- packages/sdk/lib/src/crypto/aes.rs | 99 +++++++++ packages/sdk/lib/src/crypto/argon2.rs | 214 +++++++++++++++++++ packages/sdk/lib/src/crypto/bip32.rs | 137 ++++++++++++ packages/sdk/lib/src/crypto/bip39.rs | 158 ++++++++++++++ packages/sdk/lib/src/crypto/mod.rs | 8 + packages/sdk/lib/src/crypto/pointer_types.rs | 98 +++++++++ packages/sdk/lib/src/crypto/rng.rs | 56 +++++ packages/sdk/lib/src/crypto/salt.rs | 61 ++++++ packages/sdk/lib/src/crypto/zip32.rs | 185 ++++++++++++++++ packages/sdk/lib/src/lib.rs | 1 + 12 files changed, 1196 insertions(+), 27 deletions(-) create mode 100644 packages/sdk/lib/src/crypto/aes.rs create mode 100644 packages/sdk/lib/src/crypto/argon2.rs create mode 100644 packages/sdk/lib/src/crypto/bip32.rs create mode 100644 packages/sdk/lib/src/crypto/bip39.rs create mode 100644 packages/sdk/lib/src/crypto/mod.rs create mode 100644 packages/sdk/lib/src/crypto/pointer_types.rs create mode 100644 packages/sdk/lib/src/crypto/rng.rs create mode 100644 packages/sdk/lib/src/crypto/salt.rs create mode 100644 packages/sdk/lib/src/crypto/zip32.rs 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;