From 938b8630c2e41e9c2f35cb0db5745a384da83571 Mon Sep 17 00:00:00 2001 From: andrea Date: Tue, 17 Dec 2024 17:45:24 -0800 Subject: [PATCH] WASM: add support for mnemonics --- Cargo.lock | 1 + ironfish-rust-nodejs/src/lib.rs | 2 +- ironfish-rust-wasm/Cargo.toml | 1 + ironfish-rust-wasm/src/keys/mnemonics.rs | 65 ++++++++++++++++++++++ ironfish-rust-wasm/src/keys/mod.rs | 2 + ironfish-rust-wasm/src/keys/sapling_key.rs | 35 +++++++++++- ironfish-rust-wasm/src/keys/view_keys.rs | 34 ++++++++++- ironfish-rust/src/keys/mod.rs | 4 +- ironfish-rust/src/keys/test.rs | 2 +- ironfish-rust/src/keys/view_keys.rs | 8 +-- 10 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 ironfish-rust-wasm/src/keys/mnemonics.rs diff --git a/Cargo.lock b/Cargo.lock index ee028eb588..4f1a872265 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,6 +1653,7 @@ dependencies = [ "ironfish_zkp", "rand", "rayon", + "tiny-bip39", "wasm-bindgen", "wasm-bindgen-test", ] diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index 720fc0cd63..f195353d12 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -109,7 +109,7 @@ pub fn spending_key_to_words(private_key: String, language_code: LanguageCode) - #[napi] pub fn words_to_spending_key(words: String, language_code: LanguageCode) -> Result { - let key = SaplingKey::from_words(words, language_code.into()).map_err(to_napi_err)?; + let key = SaplingKey::from_words(&words, language_code.into()).map_err(to_napi_err)?; Ok(key.hex_spending_key()) } diff --git a/ironfish-rust-wasm/Cargo.toml b/ironfish-rust-wasm/Cargo.toml index fbc0276266..470159046b 100644 --- a/ironfish-rust-wasm/Cargo.toml +++ b/ironfish-rust-wasm/Cargo.toml @@ -29,6 +29,7 @@ ironfish-jubjub = "0.1.0" ironfish_zkp = { version = "0.2.0", path = "../ironfish-zkp" } rand = "0.8.5" rayon = { version = "1.8.1", features = ["web_spin_lock"] } # need to explicitly enable the `web_spin_lock` in order to run in a browser +tiny-bip39 = "1.0" wasm-bindgen = "0.2.95" [dev-dependencies] diff --git a/ironfish-rust-wasm/src/keys/mnemonics.rs b/ironfish-rust-wasm/src/keys/mnemonics.rs new file mode 100644 index 0000000000..9d69c861c2 --- /dev/null +++ b/ironfish-rust-wasm/src/keys/mnemonics.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::errors::IronfishError; +use ironfish::errors::IronfishErrorKind; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Language { + // These are the same language codes used by `bip39` + English = "en", + ChineseSimplified = "zh-hans", + ChineseTraditional = "zh-hant", + French = "fr", + Italian = "it", + Japanese = "ja", + Korean = "ko", + Spanish = "es", +} + +impl From for Language { + fn from(x: bip39::Language) -> Self { + match x { + bip39::Language::English => Self::English, + bip39::Language::ChineseSimplified => Self::ChineseSimplified, + bip39::Language::ChineseTraditional => Self::ChineseTraditional, + bip39::Language::French => Self::French, + bip39::Language::Italian => Self::Italian, + bip39::Language::Japanese => Self::Japanese, + bip39::Language::Korean => Self::Korean, + bip39::Language::Spanish => Self::Spanish, + } + } +} + +impl From for bip39::Language { + fn from(x: Language) -> Self { + match x { + Language::English => Self::English, + Language::ChineseSimplified => Self::ChineseSimplified, + Language::ChineseTraditional => Self::ChineseTraditional, + Language::French => Self::French, + Language::Italian => Self::Italian, + Language::Japanese => Self::Japanese, + Language::Korean => Self::Korean, + Language::Spanish => Self::Spanish, + Language::__Invalid => unreachable!(), + } + } +} + +#[wasm_bindgen] +impl Language { + #[wasm_bindgen(js_name = "fromLanguageCode")] + pub fn from_language_code(code: &str) -> Result { + Self::from_str(code).ok_or_else(|| IronfishErrorKind::InvalidLanguageEncoding.into()) + } + + #[wasm_bindgen(getter, js_name = "languageCode")] + pub fn language_code(self) -> String { + self.to_str().to_string() + } +} diff --git a/ironfish-rust-wasm/src/keys/mod.rs b/ironfish-rust-wasm/src/keys/mod.rs index 5d67846bdb..b6ed99d489 100644 --- a/ironfish-rust-wasm/src/keys/mod.rs +++ b/ironfish-rust-wasm/src/keys/mod.rs @@ -2,11 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +mod mnemonics; mod proof_generation_key; mod public_address; mod sapling_key; mod view_keys; +pub use mnemonics::Language; pub use proof_generation_key::ProofGenerationKey; pub use public_address::PublicAddress; pub use sapling_key::SaplingKey; diff --git a/ironfish-rust-wasm/src/keys/sapling_key.rs b/ironfish-rust-wasm/src/keys/sapling_key.rs index ce357af063..8e3479484b 100644 --- a/ironfish-rust-wasm/src/keys/sapling_key.rs +++ b/ironfish-rust-wasm/src/keys/sapling_key.rs @@ -4,7 +4,9 @@ use crate::{ errors::IronfishError, - keys::{IncomingViewKey, OutgoingViewKey, ProofGenerationKey, PublicAddress, ViewKey}, + keys::{ + IncomingViewKey, Language, OutgoingViewKey, ProofGenerationKey, PublicAddress, ViewKey, + }, wasm_bindgen_wrapper, }; use wasm_bindgen::prelude::*; @@ -45,7 +47,21 @@ impl SaplingKey { self.0.hex_spending_key() } - // TODO: to/fromWords + #[wasm_bindgen(js_name = fromWords)] + pub fn from_words(lang: Language, words: &str) -> Result { + ironfish::keys::SaplingKey::from_words(words, lang.into()) + .map(|key| key.into()) + .map_err(|err| err.into()) + } + + #[wasm_bindgen(js_name = toWords)] + pub fn to_words(&self, lang: Language) -> String { + self.0 + .to_words(lang.into()) + .expect("conversion to words failed") + .phrase() + .to_string() + } #[wasm_bindgen(getter, js_name = publicAddress)] pub fn public_address(&self) -> PublicAddress { @@ -80,7 +96,9 @@ impl SaplingKey { #[cfg(test)] mod tests { - use crate::keys::{IncomingViewKey, OutgoingViewKey, ProofGenerationKey, SaplingKey, ViewKey}; + use crate::keys::{ + IncomingViewKey, Language, OutgoingViewKey, ProofGenerationKey, SaplingKey, ViewKey, + }; use wasm_bindgen_test::wasm_bindgen_test; macro_rules! assert_serde_ok { @@ -102,4 +120,15 @@ mod tests { assert_serde_ok!(ViewKey, key.view_key()); assert_serde_ok!(ProofGenerationKey, key.proof_generation_key()); } + + #[test] + #[wasm_bindgen_test] + fn from_to_words() { + let key = SaplingKey::random(); + let lang = Language::English; + assert_eq!( + &key, + &SaplingKey::from_words(lang, key.to_words(lang).as_ref()).unwrap() + ); + } } diff --git a/ironfish-rust-wasm/src/keys/view_keys.rs b/ironfish-rust-wasm/src/keys/view_keys.rs index f4a88d5111..2e59d3d6f3 100644 --- a/ironfish-rust-wasm/src/keys/view_keys.rs +++ b/ironfish-rust-wasm/src/keys/view_keys.rs @@ -4,7 +4,7 @@ use crate::{ errors::IronfishError, - keys::PublicAddress, + keys::{Language, PublicAddress}, primitives::{Fr, PublicKey}, wasm_bindgen_wrapper, }; @@ -39,7 +39,21 @@ impl IncomingViewKey { self.0.hex_key() } - // TODO: to/fromWords + #[wasm_bindgen(js_name = fromWords)] + pub fn from_words(lang: Language, words: &str) -> Result { + ironfish::keys::IncomingViewKey::from_words(lang.language_code().as_ref(), words) + .map(|key| key.into()) + .map_err(|err| err.into()) + } + + #[wasm_bindgen(js_name = toWords)] + pub fn to_words(&self, lang: Language) -> String { + // `words_key()` may fail only if the language code is invalid, but here we're accepting + // `Language`, not an arbitrary input, so the language code is guaranteed to be valid. + self.0 + .words_key(lang.language_code().as_ref()) + .expect("conversion to words failed") + } #[wasm_bindgen(getter, js_name = publicAddress)] pub fn public_address(&self) -> PublicAddress { @@ -74,7 +88,21 @@ impl OutgoingViewKey { self.0.hex_key() } - // TODO: to/fromWords + #[wasm_bindgen(js_name = fromWords)] + pub fn from_words(lang: Language, words: &str) -> Result { + ironfish::keys::OutgoingViewKey::from_words(lang.language_code().as_ref(), words) + .map(|key| key.into()) + .map_err(|err| err.into()) + } + + #[wasm_bindgen(js_name = toWords)] + pub fn to_words(&self, lang: Language) -> String { + // `words_key()` may fail only if the language code is invalid, but here we're accepting + // `Language`, not an arbitrary input, so the language code is guaranteed to be valid. + self.0 + .words_key(lang.language_code().as_ref()) + .expect("conversion to words failed") + } } wasm_bindgen_wrapper! { diff --git a/ironfish-rust/src/keys/mod.rs b/ironfish-rust/src/keys/mod.rs index ecb5eb3b18..476ccf04be 100644 --- a/ironfish-rust/src/keys/mod.rs +++ b/ironfish-rust/src/keys/mod.rs @@ -182,8 +182,8 @@ impl SaplingKey { } /// Takes a bip-39 phrase as a string and turns it into a SaplingKey instance - pub fn from_words(words: String, language: Language) -> Result { - let mnemonic = Mnemonic::from_phrase(&words, language) + pub fn from_words(words: &str, language: Language) -> Result { + let mnemonic = Mnemonic::from_phrase(words, language) .map_err(|_| IronfishError::new(IronfishErrorKind::InvalidMnemonicString))?; let bytes = mnemonic.entropy(); let mut byte_arr = [0; SPEND_KEY_SIZE]; diff --git a/ironfish-rust/src/keys/test.rs b/ironfish-rust/src/keys/test.rs index fd46fa2f09..fba5c86fb7 100644 --- a/ironfish-rust/src/keys/test.rs +++ b/ironfish-rust/src/keys/test.rs @@ -127,6 +127,6 @@ fn test_from_and_to_words() { // Convert from words let key = - SaplingKey::from_words(words, bip39::Language::English).expect("key should be created"); + SaplingKey::from_words(&words, bip39::Language::English).expect("key should be created"); assert_eq!(key.spending_key, key_bytes); } diff --git a/ironfish-rust/src/keys/view_keys.rs b/ironfish-rust/src/keys/view_keys.rs index ad220e8841..1085ce42d1 100644 --- a/ironfish-rust/src/keys/view_keys.rs +++ b/ironfish-rust/src/keys/view_keys.rs @@ -57,10 +57,10 @@ impl IncomingViewKey { } /// Load a key from a string of words to be decoded into bytes. - pub fn from_words(language_code: &str, value: String) -> Result { + pub fn from_words(language_code: &str, value: &str) -> Result { let language = Language::from_language_code(language_code) .ok_or_else(|| IronfishError::new(IronfishErrorKind::InvalidLanguageEncoding))?; - let mnemonic = Mnemonic::from_phrase(&value, language) + let mnemonic = Mnemonic::from_phrase(value, language) .map_err(|_| IronfishError::new(IronfishErrorKind::InvalidPaymentAddress))?; let bytes = mnemonic.entropy(); let mut byte_arr = [0; 32]; @@ -227,10 +227,10 @@ impl OutgoingViewKey { } /// Load a key from a string of words to be decoded into bytes. - pub fn from_words(language_code: &str, value: String) -> Result { + pub fn from_words(language_code: &str, value: &str) -> Result { let language = Language::from_language_code(language_code) .ok_or_else(|| IronfishError::new(IronfishErrorKind::InvalidLanguageEncoding))?; - let mnemonic = Mnemonic::from_phrase(&value, language) + let mnemonic = Mnemonic::from_phrase(value, language) .map_err(|_| IronfishError::new(IronfishErrorKind::InvalidPaymentAddress))?; let bytes = mnemonic.entropy(); let mut view_key = [0; 32];