diff --git a/Cargo.lock b/Cargo.lock index 381c653e0e..431423e6b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7586,6 +7586,8 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" name = "wasm-crypto" version = "0.1.2" dependencies = [ + "bip39", + "common", "crypto", "getrandom 0.2.10", "rstest", diff --git a/wasm-crypto/Cargo.toml b/wasm-crypto/Cargo.toml index 5373c537f4..54a0f3ccc1 100644 --- a/wasm-crypto/Cargo.toml +++ b/wasm-crypto/Cargo.toml @@ -13,6 +13,9 @@ crate-type = ["cdylib"] [dependencies] crypto = { path = '../crypto' } serialization = { path = "../serialization" } +common = { path = "../common" } + +bip39 = { workspace = true, default-features = false, features = ["std", "zeroize"] } # This crate is required for rand to work with wasm. See: https://docs.rs/getrandom/latest/getrandom/#webassembly-support getrandom = { version = "0.2", features = ["js"] } diff --git a/wasm-crypto/js-bindings/crypto_test.js b/wasm-crypto/js-bindings/crypto_test.js index 6e9d489793..a094d7a3da 100644 --- a/wasm-crypto/js-bindings/crypto_test.js +++ b/wasm-crypto/js-bindings/crypto_test.js @@ -10,6 +10,10 @@ import { public_key_from_private_key, sign_message, verify_signature, + make_default_account_pubkey, + make_receiving_address, + pubkey_to_string, + Network, } from "../pkg/wasm_crypto.js"; export async function run_test() { @@ -44,4 +48,66 @@ export async function run_test() { } console.log("Tested decoding bad private key successfully"); } + + try { + const invalid_mnemonic = "asd asd"; + make_default_account_pubkey(invalid_mnemonic, Network.Mainnet); + throw new Error("Invalid mnemonic worked somehow!"); + } catch (e) { + if (!e.includes("Invalid mnemonic string")) { + throw e; + } + console.log("Tested invalid menemonic successfully"); + } + + try { + make_receiving_address(bad_priv_key, 0); + throw new Error("Invalid public key worked somehow!"); + } catch (e) { + if (!e.includes("Invalid public key encoding")) { + throw e; + } + console.log("Tested decoding bad account public key successfully"); + } + + const mnemonic = "walk exile faculty near leg neutral license matrix maple invite cupboard hat opinion excess coffee leopard latin regret document core limb crew dizzy movie"; + { + const account_pubkey = make_default_account_pubkey(mnemonic, Network.Mainnet); + console.log(`acc pubkey = ${account_pubkey}`); + + const receiving_pubkey = make_receiving_address(account_pubkey, 0); + console.log(`receiving pubkey = ${receiving_pubkey}`); + + // test bad key index + try { + make_receiving_address(account_pubkey, 1<<31); + throw new Error("Invalid key index worked somehow!"); + } catch (e) { + if (!e.includes("Invalid key index, MSB bit set")) { + throw e; + } + console.log("Tested invalid key index with set MSB bit successfully"); + } + + const address = pubkey_to_string(receiving_pubkey, Network.Mainnet); + console.log(`address = ${address}`); + if (address != "mtc1qyqmdpxk2w42w37qsdj0e8g54ysvnlvpny3svzqx") { + throw new Error("Incorrect address generated"); + } + } + + + { + // Test generating an address for Testnet + const account_pubkey = make_default_account_pubkey(mnemonic, Network.Testnet); + console.log(`acc pubkey = ${account_pubkey}`); + + const receiving_pubkey = make_receiving_address(account_pubkey, 0); + console.log(`receiving pubkey = ${receiving_pubkey}`); + const address = pubkey_to_string(receiving_pubkey, Network.Testnet); + console.log(`address = ${address}`); + if (address != "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n") { + throw new Error("Incorrect address generated"); + } + } } diff --git a/wasm-crypto/src/error.rs b/wasm-crypto/src/error.rs index 378fa2743b..6f1ff34fcd 100644 --- a/wasm-crypto/src/error.rs +++ b/wasm-crypto/src/error.rs @@ -25,6 +25,10 @@ pub enum Error { InvalidPublicKeyEncoding, #[error("Invalid signature encoding")] InvalidSignatureEncoding, + #[error("Invalid mnemonic string")] + InvalidMnemonic, + #[error("Invalid key index, MSB bit set")] + InvalidKeyIndex, } // This is required to make an error readable in JavaScript diff --git a/wasm-crypto/src/lib.rs b/wasm-crypto/src/lib.rs index 13153189d8..5eadabc31d 100644 --- a/wasm-crypto/src/lib.rs +++ b/wasm-crypto/src/lib.rs @@ -13,19 +13,112 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crypto::key::{KeyKind, PrivateKey, PublicKey, Signature}; +pub use bip39::{Language, Mnemonic}; +use common::{ + address::{pubkeyhash::PublicKeyHash, Address}, + chain::{ + config::{Builder, ChainType, BIP44_PATH}, + Destination, + }, +}; +use crypto::key::{ + extended::{ExtendedKeyKind, ExtendedPrivateKey, ExtendedPublicKey}, + hdkd::{child_number::ChildNumber, derivable::Derivable, u31::U31}, + KeyKind, PrivateKey, PublicKey, Signature, +}; use error::Error; use serialization::{DecodeAll, Encode}; use wasm_bindgen::prelude::*; pub mod error; +#[wasm_bindgen] +pub enum Network { + Mainnet, + Testnet, + Regtest, + Signet, +} + +impl From<Network> for ChainType { + fn from(value: Network) -> Self { + match value { + Network::Mainnet => ChainType::Mainnet, + Network::Testnet => ChainType::Testnet, + Network::Regtest => ChainType::Regtest, + Network::Signet => ChainType::Signet, + } + } +} + #[wasm_bindgen] pub fn make_private_key() -> Vec<u8> { let key = PrivateKey::new_from_entropy(KeyKind::Secp256k1Schnorr); key.0.encode() } +#[wasm_bindgen] +pub fn make_default_account_pubkey(mnemonic: &str, network: Network) -> Result<Vec<u8>, Error> { + let mnemonic = bip39::Mnemonic::parse_in(Language::English, mnemonic) + .map_err(|_| Error::InvalidMnemonic)?; + let seed = mnemonic.to_seed(""); + + let root_key = ExtendedPrivateKey::new_master(&seed, ExtendedKeyKind::Secp256k1Schnorr) + .expect("Should not fail to create a master key"); + + let chain_config = Builder::new(network.into()).build(); + + let account_index = U31::ZERO; + let path = vec![ + BIP44_PATH, + chain_config.bip44_coin_type(), + ChildNumber::from_hardened(account_index), + ]; + let account_path = path.try_into().expect("Path creation should not fail"); + let account_privkey = root_key + .derive_absolute_path(&account_path) + .expect("Should not fail to derive path"); + + Ok(account_privkey.to_public_key().encode()) +} + +#[wasm_bindgen] +pub fn make_receiving_address(public_key_bytes: &[u8], key_index: u32) -> Result<Vec<u8>, Error> { + const RECEIVE_FUNDS_INDEX: ChildNumber = ChildNumber::from_normal(U31::from_u32_with_msb(0).0); + + let account_pubkey = ExtendedPublicKey::decode_all(&mut &public_key_bytes[..]) + .map_err(|_| Error::InvalidPublicKeyEncoding)?; + + let receive_funds_pkey = account_pubkey + .derive_child(RECEIVE_FUNDS_INDEX) + .expect("Should not fail to derive key"); + + let public_key: PublicKey = receive_funds_pkey + .derive_child(ChildNumber::from_normal( + U31::from_u32(key_index).ok_or(Error::InvalidKeyIndex)?, + )) + .expect("Should not fail to derive key") + .into_public_key(); + + Ok(public_key.encode()) +} + +#[wasm_bindgen] +pub fn pubkey_to_string(public_key_bytes: &[u8], network: Network) -> Result<String, Error> { + let public_key = PublicKey::decode_all(&mut &public_key_bytes[..]) + .map_err(|_| Error::InvalidPublicKeyEncoding)?; + let chain_config = Builder::new(network.into()).build(); + + let public_key_hash = PublicKeyHash::from(&public_key); + + Ok( + Address::new(&chain_config, &Destination::Address(public_key_hash)) + .expect("Should not fail to create address") + .get() + .to_owned(), + ) +} + #[wasm_bindgen] pub fn public_key_from_private_key(private_key: &[u8]) -> Result<Vec<u8>, Error> { let private_key = PrivateKey::decode_all(&mut &private_key[..])