From 71fd0a1c3ad0632a0d649137ebc728a2e2a70b65 Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Mon, 12 Sep 2022 12:39:34 -0400 Subject: [PATCH] crypto: Add BIP32 derivation to keygen --- Cargo.lock | 284 +++++++----------- crates/sui-sdk/Cargo.toml | 4 +- crates/sui-sdk/src/crypto.rs | 61 ++-- crates/sui-sdk/tests/tests.rs | 15 +- crates/sui-types/Cargo.toml | 2 + crates/sui-types/src/crypto.rs | 105 +++++++ crates/sui/Cargo.toml | 1 + crates/sui/src/client_commands.rs | 18 +- crates/sui/src/keytool.rs | 35 ++- crates/sui/src/sui_commands.rs | 18 +- crates/sui/src/unit_tests/cli_tests.rs | 2 + crates/sui/src/unit_tests/keytool_tests.rs | 148 +++++++++ crates/workspace-hack/Cargo.toml | 51 ++-- pnpm-lock.yaml | 23 ++ .../src/cryptography/ed25519-keypair.ts | 5 +- wallet/package.json | 2 + .../src/shared/cryptography/mnemonics.test.ts | 89 +++++- wallet/src/shared/cryptography/mnemonics.ts | 92 +++++- wallet/src/types/bip39-light.d.ts | 4 + 19 files changed, 663 insertions(+), 296 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74ed71bd6d374..1d6a4283638af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,15 +426,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -609,15 +600,21 @@ dependencies = [ ] [[package]] -name = "bip39" -version = "1.0.1" -source = "git+https://github.com/patrickkuo/rust-bip39.git?rev=a76fe8310416555e6383b42b8acc4eb93c7bcc89#a76fe8310416555e6383b42b8acc4eb93c7bcc89" +name = "bip32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30ed1d6f8437a487a266c8293aeb95b61a23261273e3e02912cdb8b68bf798b" dependencies = [ - "bitcoin_hashes 0.9.7", - "rand 0.6.5", - "rand_core 0.4.2", - "serde 1.0.144", - "unicode-normalization", + "bs58", + "hmac", + "k256", + "once_cell", + "pbkdf2", + "rand_core 0.6.3", + "ripemd", + "sha2 0.10.5", + "subtle", + "zeroize", ] [[package]] @@ -635,12 +632,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitcoin_hashes" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" - [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -753,6 +744,9 @@ name = "bs58" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] [[package]] name = "bstr" @@ -1346,7 +1340,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ - "autocfg 1.1.0", + "autocfg", "cfg-if 1.0.0", "crossbeam-utils", "lazy_static 1.4.0", @@ -2361,12 +2355,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures" version = "0.3.24" @@ -2808,6 +2796,12 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "hmac-sha512" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e806677ce663d0a199541030c816847b36e8dc095f70dae4a4f4ad63da5383" + [[package]] name = "home" version = "0.5.3" @@ -3045,7 +3039,7 @@ version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ - "autocfg 1.1.0", + "autocfg", "hashbrown", "serde 1.0.144", ] @@ -3528,7 +3522,7 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ - "autocfg 1.1.0", + "autocfg", "scopeguard", ] @@ -3584,7 +3578,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -4735,7 +4729,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits 0.2.15", ] @@ -4755,7 +4749,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-traits 0.2.15", ] @@ -4765,7 +4759,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits 0.2.15", ] @@ -4776,7 +4770,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-bigint", "num-integer", "num-traits 0.2.15", @@ -4797,7 +4791,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ - "autocfg 1.1.0", + "autocfg", "libm", ] @@ -4915,7 +4909,7 @@ version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ - "autocfg 1.1.0", + "autocfg", "cc", "libc", "pkg-config", @@ -5125,6 +5119,15 @@ dependencies = [ "camino", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -5554,7 +5557,7 @@ dependencies = [ "quick-error 2.0.1", "rand 0.8.5", "rand_chacha 0.3.1", - "rand_xorshift 0.3.0", + "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", @@ -5740,25 +5743,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.8", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift 0.1.1", - "winapi", -] - [[package]] name = "rand" version = "0.7.3" @@ -5769,7 +5753,7 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc 0.2.0", + "rand_hc", ] [[package]] @@ -5783,16 +5767,6 @@ dependencies = [ "rand_core 0.6.3", ] -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", -] - [[package]] name = "rand_chacha" version = "0.2.2" @@ -5813,21 +5787,6 @@ dependencies = [ "rand_core 0.6.3", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -5856,15 +5815,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rand_hc" version = "0.2.0" @@ -5874,59 +5824,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rand_xorshift" version = "0.3.0" @@ -5951,7 +5848,7 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ - "autocfg 1.1.0", + "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -5981,15 +5878,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "read-write-set" version = "0.1.0" @@ -6033,7 +5921,7 @@ name = "real_tokio" version = "1.20.1" source = "git+https://github.com/mystenmark/tokio-madsim-fork.git?rev=8ca4c94029ac1b7c8342720820e6100e9f31a372#8ca4c94029ac1b7c8342720820e6100e9f31a372" dependencies = [ - "autocfg 1.1.0", + "autocfg", "bytes", "libc", "memchr", @@ -6198,6 +6086,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1facec54cb5e0dc08553501fa740091086d0259ad0067e0d4103448e4cb22ed3" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "roaring" version = "0.10.1" @@ -6459,7 +6356,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff" dependencies = [ - "bitcoin_hashes 0.11.0", + "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", ] @@ -6920,7 +6817,16 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" dependencies = [ - "autocfg 1.1.0", + "autocfg", +] + +[[package]] +name = "slip10_ed25519" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be0ff28bf14f9610a342169084e87a4f435ad798ec528dc7579a3678fa9dc9a" +dependencies = [ + "hmac-sha512", ] [[package]] @@ -7234,6 +7140,7 @@ dependencies = [ "async-trait", "base64ct", "bcs", + "bip32", "camino", "clap 3.2.21", "colored", @@ -7790,7 +7697,7 @@ dependencies = [ "async-recursion", "async-trait", "bcs", - "bip39", + "bip32", "clap 3.2.21", "dirs", "futures", @@ -7812,6 +7719,7 @@ dependencies = [ "sui-json-rpc-types", "sui-types", "tempfile", + "tiny-bip39", "tokio", "workspace-hack 0.1.0", ] @@ -7974,6 +7882,7 @@ dependencies = [ "base64ct", "bcs", "bincode", + "bip32", "byteorder", "curve25519-dalek", "digest 0.10.3", @@ -8005,6 +7914,7 @@ dependencies = [ "sha2 0.9.9", "sha3 0.10.4", "signature", + "slip10_ed25519", "static_assertions", "strum", "strum_macros", @@ -8397,6 +8307,25 @@ dependencies = [ "lazy_static 0.2.11", ] +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.8.5", + "rustc-hash", + "sha2 0.10.5", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8428,7 +8357,7 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ - "autocfg 1.1.0", + "autocfg", "bytes", "libc", "memchr", @@ -9577,8 +9506,7 @@ dependencies = [ "atoi", "atomicwrites", "atty", - "autocfg 0.1.8", - "autocfg 1.1.0", + "autocfg", "axum", "axum-core", "backoff", @@ -9593,11 +9521,10 @@ dependencies = [ "bimap", "bincode", "bindgen", - "bip39", + "bip32", "bit-set", "bit-vec", - "bitcoin_hashes 0.11.0", - "bitcoin_hashes 0.9.7", + "bitcoin_hashes", "bitflags", "bitmaps", "blake2", @@ -9792,6 +9719,7 @@ dependencies = [ "hex-literal", "hkdf", "hmac", + "hmac-sha512", "home", "http", "http-body", @@ -9969,6 +9897,7 @@ dependencies = [ "parse-zoneinfo", "paste", "pathdiff", + "pbkdf2", "peeking_take_while", "pem", "percent-encoding", @@ -10024,24 +9953,14 @@ dependencies = [ "quote 0.6.13", "quote 1.0.21", "radix_trie", - "rand 0.6.5", "rand 0.7.3", "rand 0.8.5", - "rand_chacha 0.1.1", "rand_chacha 0.2.2", "rand_chacha 0.3.1", - "rand_core 0.3.1", - "rand_core 0.4.2", "rand_core 0.5.1", "rand_core 0.6.3", "rand_distr", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift 0.1.1", - "rand_xorshift 0.3.0", + "rand_xorshift", "rand_xoshiro", "rayon", "rayon-core", @@ -10059,6 +9978,7 @@ dependencies = [ "retain_mut", "rfc6979", "ring", + "ripemd", "roaring", "rocksdb", "rust-ini", @@ -10130,6 +10050,7 @@ dependencies = [ "siphasher", "sized-chunks", "slab", + "slip10_ed25519", "slug", "smallvec", "smawk", @@ -10188,6 +10109,7 @@ dependencies = [ "time 0.3.14", "time-macros", "tint", + "tiny-bip39", "tinytemplate", "tinyvec", "tinyvec_macros", @@ -10331,7 +10253,7 @@ dependencies = [ "async-stream-impl", "async-trait", "atty", - "autocfg 1.1.0", + "autocfg", "axum", "axum-core", "backoff", @@ -10343,7 +10265,7 @@ dependencies = [ "bindgen", "bit-set", "bit-vec", - "bitcoin_hashes 0.11.0", + "bitcoin_hashes", "bitflags", "blake2", "blake2s_simd", @@ -10571,7 +10493,7 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.5.1", "rand_core 0.6.3", - "rand_xorshift 0.3.0", + "rand_xorshift", "rayon", "rayon-core", "rcgen", diff --git a/crates/sui-sdk/Cargo.toml b/crates/sui-sdk/Cargo.toml index 4cd0e16d476c6..be7605145702c 100644 --- a/crates/sui-sdk/Cargo.toml +++ b/crates/sui-sdk/Cargo.toml @@ -18,8 +18,8 @@ signature = "1.6.0" tokio = "1.20.1" rand = "0.8.5" bcs = "0.1.3" - -bip39 = { git = "https://github.com/patrickkuo/rust-bip39.git" , rev = "a76fe8310416555e6383b42b8acc4eb93c7bcc89", features = ["rand"]} +tiny-bip39 = "1.0.0" +bip32 = "0.4.0" sui-json-rpc = { path = "../sui-json-rpc" } sui-json-rpc-types= { path = "../sui-json-rpc-types" } diff --git a/crates/sui-sdk/src/crypto.rs b/crates/sui-sdk/src/crypto.rs index c5516dde17e72..c205ea4b763b1 100644 --- a/crates/sui-sdk/src/crypto.rs +++ b/crates/sui-sdk/src/crypto.rs @@ -1,10 +1,9 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -// TODO: Remove usage of rand::rngs::adapter::ReadRng. -#![allow(deprecated)] - use anyhow::anyhow; +use bip32::DerivationPath; +use bip39::{Language, Mnemonic, MnemonicType, Seed}; use rand::{rngs::StdRng, SeedableRng}; use serde::{Deserialize, Serialize}; use signature::Signer; @@ -15,15 +14,11 @@ use std::fs; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use bip39::Mnemonic; -use rand::rngs::adapter::ReadRng; use sui_types::base_types::SuiAddress; use sui_types::crypto::{ - get_key_pair_from_rng, random_key_pair_by_type_from_rng, EncodeDecodeBase64, PublicKey, - Signature, SignatureScheme, SuiKeyPair, + derive_key_pair_from_path, get_key_pair_from_rng, EncodeDecodeBase64, PublicKey, Signature, + SignatureScheme, SuiKeyPair, }; #[derive(Serialize, Deserialize)] @@ -157,15 +152,17 @@ impl SuiKeystore { pub fn generate_new_key( &mut self, key_scheme: SignatureScheme, + derivation_path: Option, ) -> Result<(SuiAddress, String, SignatureScheme), anyhow::Error> { - let mnemonic = Mnemonic::generate(12)?; - let seed = mnemonic.to_seed(""); - let mut rng = RngWrapper(ReadRng::new(&seed)); - match random_key_pair_by_type_from_rng(key_scheme, &mut rng) { - Ok((address, kp)) => { - let k = kp.public(); - self.0.add_key(kp)?; - Ok((address, mnemonic.to_string(), k.scheme())) + let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); + match derive_key_pair_from_path( + Seed::new(&mnemonic, "").as_bytes(), + derivation_path, + &key_scheme, + ) { + Ok((address, keypair)) => { + self.add_key(keypair)?; + Ok((address, mnemonic.phrase().to_string(), key_scheme)) } Err(e) => Err(anyhow!("error generating key {:?}", e)), } @@ -187,10 +184,12 @@ impl SuiKeystore { &mut self, phrase: &str, key_scheme: SignatureScheme, + derivation_path: Option, ) -> Result { - let seed = &Mnemonic::from_str(phrase).unwrap().to_seed(""); - let mut rng = RngWrapper(ReadRng::new(seed)); - match random_key_pair_by_type_from_rng(key_scheme, &mut rng) { + let mnemonic = Mnemonic::from_phrase(phrase, Language::English) + .map_err(|e| anyhow::anyhow!("Invalid mnemonic phrase: {:?}", e))?; + let seed = Seed::new(&mnemonic, ""); + match derive_key_pair_from_path(seed.as_bytes(), derivation_path, &key_scheme) { Ok((address, kp)) => { self.0.add_key(kp)?; Ok(address) @@ -204,28 +203,6 @@ impl SuiKeystore { } } -/// wrapper for adding CryptoRng and RngCore impl to ReadRng. -struct RngWrapper<'a>(ReadRng<&'a [u8]>); - -impl rand::CryptoRng for RngWrapper<'_> {} -impl rand::RngCore for RngWrapper<'_> { - fn next_u32(&mut self) -> u32 { - self.0.next_u32() - } - - fn next_u64(&mut self) -> u64 { - self.0.next_u64() - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest) - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { - self.0.try_fill_bytes(dest) - } -} - struct KeystoreSigner<'a> { keystore: &'a dyn AccountKeystore, address: SuiAddress, diff --git a/crates/sui-sdk/tests/tests.rs b/crates/sui-sdk/tests/tests.rs index 5d662fae04590..fd6f06da7bba7 100644 --- a/crates/sui-sdk/tests/tests.rs +++ b/crates/sui-sdk/tests/tests.rs @@ -16,13 +16,14 @@ fn mnemonic_test() { let temp_dir = TempDir::new().unwrap(); let keystore_path = temp_dir.path().join("sui.keystore"); let mut keystore = KeystoreType::File(keystore_path).init().unwrap(); - - let (address, phrase, scheme) = keystore.generate_new_key(SignatureScheme::ED25519).unwrap(); + let (address, phrase, scheme) = keystore + .generate_new_key(SignatureScheme::ED25519, None) + .unwrap(); let keystore_path_2 = temp_dir.path().join("sui2.keystore"); let mut keystore2 = KeystoreType::File(keystore_path_2).init().unwrap(); let imported_address = keystore2 - .import_from_mnemonic(&phrase, SignatureScheme::ED25519) + .import_from_mnemonic(&phrase, SignatureScheme::ED25519, None) .unwrap(); assert_eq!(scheme.flag(), Ed25519SuiSignature::SCHEME.flag()); assert_eq!(address, imported_address); @@ -31,22 +32,22 @@ fn mnemonic_test() { /// This test confirms rust's implementation of mnemonic is the same with the Sui Wallet #[test] fn sui_wallet_address_mnemonic_test() -> Result<(), anyhow::Error> { - // Recovery phase and SuiAddress obtained from Sui wallet v0.0.4 (prior key flag changes) - let phrase = "oil puzzle immense upon pony govern jelly neck portion laptop laptop wall"; - let expected_address = SuiAddress::from_str("0x6a06dd564dfb2f0c71f3e167a48f569c705ed34c")?; + let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + let expected_address = SuiAddress::from_str("0x1a4623343cd42be47d67314fce0ad042f3c82685")?; let temp_dir = TempDir::new().unwrap(); let keystore_path = temp_dir.path().join("sui.keystore"); let mut keystore = KeystoreType::File(keystore_path).init().unwrap(); keystore - .import_from_mnemonic(phrase, SignatureScheme::ED25519) + .import_from_mnemonic(phrase, SignatureScheme::ED25519, None) .unwrap(); let pubkey = keystore.keys()[0].clone(); assert_eq!(pubkey.flag(), Ed25519SuiSignature::SCHEME.flag()); let mut hasher = Sha3_256::default(); + hasher.update([pubkey.flag()]); hasher.update(pubkey); let g_arr = hasher.finalize(); let mut res = [0u8; SUI_ADDRESS_LENGTH]; diff --git a/crates/sui-types/Cargo.toml b/crates/sui-types/Cargo.toml index 385206ceef94d..b0cdbae40e208 100644 --- a/crates/sui-types/Cargo.toml +++ b/crates/sui-types/Cargo.toml @@ -38,6 +38,8 @@ strum_macros = "^0.24" roaring = "0.10.1" enum_dispatch = "^0.3" eyre = "0.6.8" +bip32 = "0.4.0" +slip10_ed25519 = "0.1.3" name-variant = "0.1.0" typed-store = "0.1.0" diff --git a/crates/sui-types/src/crypto.rs b/crates/sui-types/src/crypto.rs index a1db0972d02f2..a55183f7f4a8f 100644 --- a/crates/sui-types/src/crypto.rs +++ b/crates/sui-types/src/crypto.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use anyhow::{anyhow, Error}; use base64ct::Encoding; +use bip32::{ChildNumber, DerivationPath, XPrv}; use digest::Digest; use fastcrypto::bls12381::BLS12381PublicKey; use fastcrypto::ed25519::{ @@ -29,6 +30,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, Bytes}; use sha3::Sha3_256; use signature::Signer; +use slip10_ed25519::derive_ed25519_private_key; use crate::base_types::{AuthorityName, SuiAddress}; use crate::committee::{Committee, EpochId}; @@ -54,6 +56,9 @@ pub type NetworkPublicKey = Ed25519PublicKey; pub type NetworkPrivateKey = Ed25519PrivateKey; pub const PROOF_OF_POSSESSION_DOMAIN: &[u8] = b"kosk"; +pub const DERIVATION_PATH_COIN_TYPE: u32 = 784; +pub const DERVIATION_PATH_PURPOSE_ED25519: u32 = 44; +pub const DERVIATION_PATH_PURPOSE_SECP256K1: u32 = 54; // Creates a proof that the keypair is possesed, as well as binds this proof to a specific SuiAddress. pub fn generate_proof_of_possession( @@ -489,6 +494,106 @@ where (kp.public().into(), kp) } +/// Ed25519 follows SLIP-0010 using hardened path: m/44'/784'/0'/0'/{index}' +/// Secp256k1 follows BIP-32 using path where the first 3 levels are hardened: m/54'/784'/0'/0/{index} +/// Note that the purpose for Secp256k1 is registered as 54, to differentiate from Ed25519 with purpose 44. +pub fn derive_key_pair_from_path( + seed: &[u8], + derivation_path: Option, + key_scheme: &SignatureScheme, +) -> Result<(SuiAddress, SuiKeyPair), SuiError> { + let path = validate_path(key_scheme, derivation_path)?; + match key_scheme { + SignatureScheme::ED25519 => { + let indexes = path.into_iter().map(|i| i.into()).collect::>(); + let derived = derive_ed25519_private_key(seed, &indexes); + let sk = Ed25519PrivateKey::from_bytes(&derived) + .map_err(|e| SuiError::SignatureKeyGenError(e.to_string()))?; + let kp = Ed25519KeyPair::from(sk); + Ok((kp.public().into(), SuiKeyPair::Ed25519SuiKeyPair(kp))) + } + SignatureScheme::Secp256k1 => { + let child_xprv = XPrv::derive_from_path(&seed, &path) + .map_err(|e| SuiError::SignatureKeyGenError(e.to_string()))?; + let kp = Secp256k1KeyPair::from( + Secp256k1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice()) + .unwrap(), + ); + Ok((kp.public().into(), SuiKeyPair::Secp256k1SuiKeyPair(kp))) + } + SignatureScheme::BLS12381 => Err(SuiError::UnsupportedFeatureError { + error: "BLS is not supported for user key derivation".to_string(), + }), + } +} + +pub fn validate_path( + key_scheme: &SignatureScheme, + path: Option, +) -> Result { + match key_scheme { + SignatureScheme::ED25519 => { + match path { + Some(p) => { + // The derivation path must be hardened at all levels with purpose = 44, coin_type = 784 + if let &[purpose, coin_type, account, change, address] = p.as_ref() { + if purpose + == ChildNumber::new(DERVIATION_PATH_PURPOSE_ED25519, true).unwrap() + && coin_type + == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).unwrap() + && account.is_hardened() + && change.is_hardened() + && address.is_hardened() + { + Ok(p) + } else { + Err(SuiError::SignatureKeyGenError("Invalid path".to_string())) + } + } else { + Err(SuiError::SignatureKeyGenError("Invalid path".to_string())) + } + } + None => Ok(format!( + "m/{DERVIATION_PATH_PURPOSE_ED25519}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0'/0'" + ) + .parse() + .unwrap()), + } + } + SignatureScheme::Secp256k1 => { + match path { + Some(p) => { + // The derivation path must be hardened at first 3 levels with purpose = 54, coin_type = 784 + if let &[purpose, coin_type, account, change, address] = p.as_ref() { + if purpose + == ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256K1, true).unwrap() + && coin_type + == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).unwrap() + && account.is_hardened() + && !change.is_hardened() + && !address.is_hardened() + { + Ok(p) + } else { + Err(SuiError::SignatureKeyGenError("Invalid path".to_string())) + } + } else { + Err(SuiError::SignatureKeyGenError("Invalid path".to_string())) + } + } + None => Ok(format!( + "m/{DERVIATION_PATH_PURPOSE_SECP256K1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0" + ) + .parse() + .unwrap()), + } + } + SignatureScheme::BLS12381 => Err(SuiError::UnsupportedFeatureError { + error: "BLS is not supported for user key derivation".to_string(), + }), + } +} + /// Wrapper function to return SuiKeypair based on key scheme string with seedable rng. pub fn random_key_pair_by_type_from_rng( key_scheme: SignatureScheme, diff --git a/crates/sui/Cargo.toml b/crates/sui/Cargo.toml index 6691800e8de1c..029d38f819bf9 100644 --- a/crates/sui/Cargo.toml +++ b/crates/sui/Cargo.toml @@ -20,6 +20,7 @@ tracing = "0.1.36" bcs = "0.1.3" clap = { version = "3.2.17", features = ["derive"] } telemetry-subscribers = "0.1.0" +bip32 = "0.4.0" sui-core = { path = "../sui-core" } sui-framework = { path = "../sui-framework" } diff --git a/crates/sui/src/client_commands.rs b/crates/sui/src/client_commands.rs index 38369c7728053..fef2c4accdbd7 100644 --- a/crates/sui/src/client_commands.rs +++ b/crates/sui/src/client_commands.rs @@ -10,6 +10,7 @@ use std::{ }; use anyhow::anyhow; +use bip32::DerivationPath; use clap::*; use colored::Colorize; use move_core_types::language_storage::TypeTag; @@ -190,9 +191,13 @@ pub enum SuiClientCommands { #[clap(name = "addresses")] Addresses, - /// Generate new address and keypair with keypair scheme flag {ed25519 | secp256k1}. + /// Generate new address and keypair with keypair scheme flag {ed25519 | secp256k1} + /// with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1. #[clap(name = "new-address")] - NewAddress { key_scheme: SignatureScheme }, + NewAddress { + key_scheme: SignatureScheme, + derivation_path: Option, + }, /// Obtain all objects owned by the address. #[clap(name = "objects")] @@ -409,8 +414,13 @@ impl SuiClientCommands { SuiClientCommandResult::SyncClientState } - SuiClientCommands::NewAddress { key_scheme } => { - let (address, phrase, scheme) = context.keystore.generate_new_key(key_scheme)?; + SuiClientCommands::NewAddress { + key_scheme, + derivation_path, + } => { + let (address, phrase, scheme) = context + .keystore + .generate_new_key(key_scheme, derivation_path)?; SuiClientCommandResult::NewAddress((address, phrase, scheme)) } SuiClientCommands::Gas { address } => { diff --git a/crates/sui/src/keytool.rs b/crates/sui/src/keytool.rs index b05010445a08b..7e042279cac62 100644 --- a/crates/sui/src/keytool.rs +++ b/crates/sui/src/keytool.rs @@ -6,8 +6,10 @@ use std::path::{Path, PathBuf}; use anyhow::anyhow; use base64ct::Encoding as _; +use bip32::{DerivationPath, Mnemonic}; use clap::*; use fastcrypto::traits::{ToFromBytes, VerifyingKey}; +use signature::rand_core::OsRng; use tracing::info; use fastcrypto::ed25519::{Ed25519KeyPair, Ed25519PrivateKey, Ed25519PublicKey}; @@ -15,7 +17,7 @@ use sui_sdk::crypto::SuiKeystore; use sui_types::base_types::SuiAddress; use sui_types::base_types::{decode_bytes_hex, encode_bytes_hex}; use sui_types::crypto::{ - get_key_pair, random_key_pair_by_type, AuthorityKeyPair, Ed25519SuiSignature, + derive_key_pair_from_path, get_key_pair, AuthorityKeyPair, Ed25519SuiSignature, EncodeDecodeBase64, NetworkKeyPair, SignatureScheme, SuiKeyPair, SuiSignatureInner, }; use sui_types::sui_serde::{Base64, Encoding}; @@ -28,9 +30,12 @@ mod keytool_tests; #[derive(Subcommand)] #[clap(rename_all = "kebab-case")] pub enum KeyToolCommand { - /// Generate a new keypair with keypair scheme flag {ed25519 | secp256k1}. Output file to current dir (to generate keypair to sui.keystore, use `sui client new-address`) + /// Generate a new keypair with keypair scheme flag {ed25519 | secp256k1} + /// with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1. + /// And output file to current dir (to generate keypair and add to sui.keystore, use `sui client new-address`) Generate { key_scheme: SignatureScheme, + derivation_path: Option, }, Show { file: PathBuf, @@ -48,10 +53,12 @@ pub enum KeyToolCommand { #[clap(long)] data: String, }, - /// Import mnemonic phrase and generate keypair based on key scheme flag {ed25519 | secp256k1}. + /// Import mnemonic phrase and generate keypair based on key scheme flag {ed25519 | secp256k1} + /// with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1. Import { mnemonic_phrase: String, key_scheme: SignatureScheme, + derivation_path: Option, }, /// This is a temporary helper function to ensure that testnet genesis does not break while /// we transition towards BLS signatures. @@ -63,26 +70,28 @@ pub enum KeyToolCommand { impl KeyToolCommand { pub fn execute(self, keystore: &mut SuiKeystore) -> Result<(), anyhow::Error> { match self { - KeyToolCommand::Generate { key_scheme } => { + KeyToolCommand::Generate { + key_scheme, + derivation_path, + } => { let k = key_scheme.to_string(); if "bls12381" == key_scheme.to_string() { let (address, keypair): (_, AuthorityKeyPair) = get_key_pair(); let file_name = format!("bls-{address}.key"); write_authority_keypair_to_file(&keypair, &file_name)?; } else { - match random_key_pair_by_type(key_scheme) { - Ok((address, keypair)) => { + let mnemonic = Mnemonic::random(&mut OsRng, Default::default()); + let seed = mnemonic.to_seed(""); + match derive_key_pair_from_path(seed.as_bytes(), derivation_path, &key_scheme) { + Ok((address, kp)) => { let file_name = format!("{address}.key"); - write_keypair_to_file(&keypair, &file_name)?; + write_keypair_to_file(&kp, &file_name)?; println!("{:?} key generated and saved to '{file_name}'", k); } - Err(e) => { - println!("Failed to generate keypair: {:?}", e) - } + Err(e) => println!("Failed to generate keypair: {:?}", e), } } } - KeyToolCommand::Show { file } => { let res: Result = read_keypair_from_file(&file); match res { @@ -138,8 +147,10 @@ impl KeyToolCommand { KeyToolCommand::Import { mnemonic_phrase, key_scheme, + derivation_path, } => { - let address = keystore.import_from_mnemonic(&mnemonic_phrase, key_scheme)?; + let address = + keystore.import_from_mnemonic(&mnemonic_phrase, key_scheme, derivation_path)?; info!("Key imported for address [{address}]"); } diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 903b0ac7f6727..f88d509421c5a 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1,6 +1,13 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::client_commands::{SuiClientCommands, WalletContext}; +use crate::config::SuiClientConfig; +use crate::console::start_console; +use crate::genesis_ceremony::{run, Ceremony}; +use crate::keytool::KeyToolCommand; +use crate::sui_move::{self, execute_move_command}; +use move_package::BuildConfig; use std::io::{stderr, stdout, Write}; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; @@ -8,7 +15,6 @@ use std::{fs, io}; use anyhow::{anyhow, bail}; use clap::*; -use move_package::BuildConfig; use tracing::info; use sui_config::gateway::GatewayConfig; @@ -23,13 +29,6 @@ use sui_sdk::ClientType; use sui_swarm::memory::Swarm; use sui_types::crypto::{KeypairTraits, SignatureScheme, SuiKeyPair}; -use crate::client_commands::{SuiClientCommands, WalletContext}; -use crate::config::SuiClientConfig; -use crate::console::start_console; -use crate::genesis_ceremony::{run, Ceremony}; -use crate::keytool::KeyToolCommand; -use crate::sui_move::{self, execute_move_command}; - #[allow(clippy::large_enum_variant)] #[derive(Parser)] #[clap( @@ -413,7 +412,8 @@ async fn prompt_if_no_config(wallet_conf_path: &Path) -> Result<(), anyhow::Erro Ok(s) => s, Err(e) => return Err(anyhow!("{e}")), }; - let (new_address, phrase, scheme) = keystore.init()?.generate_new_key(key_scheme)?; + let (new_address, phrase, scheme) = + keystore.init()?.generate_new_key(key_scheme, None)?; println!( "Generated new keypair for address with scheme {:?} [{new_address}]", scheme.to_string() diff --git a/crates/sui/src/unit_tests/cli_tests.rs b/crates/sui/src/unit_tests/cli_tests.rs index eedcd4b83f479..76b190036d178 100644 --- a/crates/sui/src/unit_tests/cli_tests.rs +++ b/crates/sui/src/unit_tests/cli_tests.rs @@ -767,6 +767,7 @@ async fn test_switch_command() -> Result<(), anyhow::Error> { // Create a new address let os = SuiClientCommands::NewAddress { key_scheme: SignatureScheme::ED25519, + derivation_path: None, } .execute(&mut context) .await?; @@ -820,6 +821,7 @@ async fn test_new_address_command_by_flag() -> Result<(), anyhow::Error> { SuiClientCommands::NewAddress { key_scheme: SignatureScheme::Secp256k1, + derivation_path: None, } .execute(&mut context) .await?; diff --git a/crates/sui/src/unit_tests/keytool_tests.rs b/crates/sui/src/unit_tests/keytool_tests.rs index 1f92690f055b4..c34a313a0b808 100644 --- a/crates/sui/src/unit_tests/keytool_tests.rs +++ b/crates/sui/src/unit_tests/keytool_tests.rs @@ -17,6 +17,7 @@ use sui_types::crypto::Ed25519SuiSignature; use sui_types::crypto::EncodeDecodeBase64; use sui_types::crypto::Secp256k1SuiSignature; use sui_types::crypto::Signature; +use sui_types::crypto::SignatureScheme; use sui_types::crypto::SuiKeyPair; use sui_types::crypto::SuiSignatureInner; use tempfile::TempDir; @@ -136,3 +137,150 @@ fn test_load_keystore_err() { // cannot load keypair due to missing flag assert!(KeystoreType::File(path2).init().is_err()); } + +#[test] +fn test_mnemonics_ed25519() -> Result<(), anyhow::Error> { + // Test case matches with /sui/wallet/src/shared/cryptography/mnemonics.test.ts + let mut keystore = KeystoreType::InMem(0).init().unwrap(); + let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: None, + } + .execute(&mut keystore)?; + keystore.keys().iter().for_each(|pk| { + assert_eq!( + hex::encode(pk.as_ref()), + "685b2d6f98784dd763249af21c92f588ca1be80c40a98c55bf7c91b74e5ac1e2" + ); + }); + keystore.addresses().iter().for_each(|addr| { + assert_eq!( + addr.to_string(), + "0x1a4623343cd42be47d67314fce0ad042f3c82685" + ); + }); + Ok(()) +} + +#[test] +fn test_mnemonics_secp256k1() -> Result<(), anyhow::Error> { + // Test case generated from https://microbitcoinorg.github.io/mnemonic/ with path m/54'/784'/0'/0/0 + let mut keystore = KeystoreType::InMem(0).init().unwrap(); + let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::Secp256k1, + derivation_path: None, + } + .execute(&mut keystore)?; + keystore.keys().iter().for_each(|pk| { + assert_eq!( + hex::encode(pk.as_ref()), + "03e3717435582ab33d2e315d21e9bc4e19500a1fc4c8cdc73a15365891774b131f" + ); + }); + keystore.addresses().iter().for_each(|addr| { + assert_eq!( + addr.to_string(), + "0xed17b3f435c03ff69c2cdc6d394932e68375f20f" + ); + }); + Ok(()) +} + +#[test] +fn test_invalid_derivation_path() -> Result<(), anyhow::Error> { + let mut keystore = KeystoreType::InMem(0).init().unwrap(); + let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/44'/1'/0'/0/0".parse().unwrap()), + } + .execute(&mut keystore) + .is_err()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/0'/784'/0'/0/0".parse().unwrap()), + } + .execute(&mut keystore) + .is_err()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/54'/784'/0'/0/0".parse().unwrap()), + } + .execute(&mut keystore) + .is_err()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::Secp256k1, + derivation_path: Some("m/54'/784'/0'/0'/0'".parse().unwrap()), + } + .execute(&mut keystore) + .is_err()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::Secp256k1, + derivation_path: Some("m/44'/784'/0'/0/0".parse().unwrap()), + } + .execute(&mut keystore) + .is_err()); + + Ok(()) +} + +#[test] +fn test_valid_derivation_path() -> Result<(), anyhow::Error> { + let mut keystore = KeystoreType::InMem(0).init().unwrap(); + let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss"; + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/44'/784'/0'/0'/0'".parse().unwrap()), + } + .execute(&mut keystore) + .is_ok()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/44'/784'/0'/0'/1'".parse().unwrap()), + } + .execute(&mut keystore) + .is_ok()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: Some("m/44'/784'/1'/0'/1'".parse().unwrap()), + } + .execute(&mut keystore) + .is_ok()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::Secp256k1, + derivation_path: Some("m/54'/784'/0'/0/1".parse().unwrap()), + } + .execute(&mut keystore) + .is_ok()); + + assert!(KeyToolCommand::Import { + mnemonic_phrase: phrase.to_string(), + key_scheme: SignatureScheme::Secp256k1, + derivation_path: Some("m/54'/784'/1'/0/1".parse().unwrap()), + } + .execute(&mut keystore) + .is_ok()); + Ok(()) +} diff --git a/crates/workspace-hack/Cargo.toml b/crates/workspace-hack/Cargo.toml index 7997165816808..9264cb32f7f63 100644 --- a/crates/workspace-hack/Cargo.toml +++ b/crates/workspace-hack/Cargo.toml @@ -54,11 +54,10 @@ beef = { version = "0.5", features = ["impl_serde", "serde"] } better_any = { version = "0.1", default-features = false } bimap = { version = "0.6", features = ["std"] } bincode = { version = "1", default-features = false } -bip39 = { git = "https://github.com/patrickkuo/rust-bip39.git", rev = "a76fe8310416555e6383b42b8acc4eb93c7bcc89", features = ["rand", "serde", "std", "unicode-normalization"] } +bip32 = { version = "0.4", features = ["alloc", "bip39", "k256", "mnemonic", "once_cell", "pbkdf2", "secp256k1", "std"] } bit-set = { version = "0.5", features = ["std"] } bit-vec = { version = "0.6", default-features = false, features = ["std"] } -bitcoin_hashes-a6292c17cd707f01 = { package = "bitcoin_hashes", version = "0.11", default-features = false } -bitcoin_hashes-274715c4dabd11b0 = { package = "bitcoin_hashes", version = "0.9", features = ["std"] } +bitcoin_hashes = { version = "0.11", default-features = false } bitflags = { version = "1" } bitmaps = { version = "2", features = ["std"] } blake2 = { version = "0.9", features = ["std"] } @@ -68,7 +67,7 @@ block-buffer-274715c4dabd11b0 = { package = "block-buffer", version = "0.9", def block-padding = { version = "0.2", default-features = false } bls-crypto = { git = "https://github.com/huitseeker/celo-bls-snark-rs", branch = "updates-2", features = ["compat"] } blst = { version = "0.3" } -bs58 = { version = "0.4", features = ["alloc", "std"] } +bs58 = { version = "0.4", features = ["alloc", "check", "sha2", "std"] } bstr = { version = "0.2", features = ["lazy_static", "regex-automata", "serde", "serde1", "serde1-nostd", "std", "unicode"] } bulletproofs = { version = "4", features = ["rand", "std", "thiserror"] } bytecode-interpreter-crypto = { git = "https://github.com/move-language/move", rev = "e1e647b73dbd3652aabb2020728a4a517c26e28e", features = ["fiat"] } @@ -217,6 +216,7 @@ heck-468e82937335b1c9 = { package = "heck", version = "0.3", default-features = hex = { version = "0.4", features = ["alloc", "std"] } hkdf = { version = "0.12", default-features = false, features = ["std"] } hmac = { version = "0.12", default-features = false, features = ["reset", "std"] } +hmac-sha512 = { version = "0.1", features = ["sha384"] } http = { version = "0.2", default-features = false } http-body = { version = "0.4", default-features = false } http-range-header = { version = "0.3", default-features = false } @@ -360,6 +360,7 @@ parking_lot_core-ca01ad9e24f5d932 = { package = "parking_lot_core", version = "0 parking_lot_core-c38e5c1d305a1b54 = { package = "parking_lot_core", version = "0.8", default-features = false } parking_lot_core-274715c4dabd11b0 = { package = "parking_lot_core", version = "0.9", default-features = false } pathdiff = { version = "0.2", default-features = false, features = ["camino"] } +pbkdf2 = { version = "0.11", default-features = false } pem = { version = "1", default-features = false } percent-encoding = { version = "2", features = ["alloc"] } pest = { version = "2", features = ["std", "thiserror"] } @@ -395,23 +396,13 @@ quinn-proto = { version = "0.8", default-features = false, features = ["ring", " quinn-udp = { version = "0.1", default-features = false } quote-dff4ba8e3ae991db = { package = "quote", version = "1", features = ["proc-macro"] } radix_trie = { version = "0.2", default-features = false } -rand-3b31131e45eafb45 = { package = "rand", version = "0.6", features = ["alloc", "rand_os", "std"] } rand-ca01ad9e24f5d932 = { package = "rand", version = "0.7", features = ["alloc", "getrandom", "getrandom_package", "libc", "std"] } rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["alloc", "getrandom", "libc", "rand_chacha", "small_rng", "std", "std_rng"] } -rand_chacha-c65f7effa3be6d31 = { package = "rand_chacha", version = "0.1", default-features = false } rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3", features = ["std"] } -rand_core-468e82937335b1c9 = { package = "rand_core", version = "0.3", default-features = false } -rand_core-9fbad63c4bcf4a8f = { package = "rand_core", version = "0.4", default-features = false, features = ["alloc", "std"] } rand_core-d8f496e17d97b5cb = { package = "rand_core", version = "0.5", default-features = false, features = ["alloc", "getrandom", "std"] } rand_core-3b31131e45eafb45 = { package = "rand_core", version = "0.6", default-features = false, features = ["alloc", "getrandom", "std"] } rand_distr = { version = "0.4", features = ["alloc", "std"] } -rand_hc = { version = "0.1", default-features = false } -rand_isaac = { version = "0.1", default-features = false } -rand_jitter = { version = "0.1", default-features = false, features = ["std"] } -rand_os = { version = "0.1", default-features = false } -rand_pcg = { version = "0.1", default-features = false } -rand_xorshift-c65f7effa3be6d31 = { package = "rand_xorshift", version = "0.1", default-features = false } -rand_xorshift-468e82937335b1c9 = { package = "rand_xorshift", version = "0.3", default-features = false } +rand_xorshift = { version = "0.3", default-features = false } rand_xoshiro = { version = "0.6", default-features = false } rayon = { version = "1", default-features = false } rayon-core = { version = "1", default-features = false } @@ -427,6 +418,7 @@ reqwest = { version = "0.11", features = ["__tls", "blocking", "default-tls", "h retain_mut = { version = "0.1", default-features = false } rfc6979 = { version = "0.3", default-features = false } ring = { version = "0.16", features = ["alloc", "dev_urandom_fallback", "once_cell"] } +ripemd = { version = "0.1", default-features = false } roaring = { version = "0.10", default-features = false } rocksdb = { version = "0.19", features = ["bzip2", "lz4", "multi-threaded-cf", "snappy", "zlib", "zstd"] } rust-ini = { version = "0.13", default-features = false } @@ -477,6 +469,7 @@ simplelog = { version = "0.9", features = ["termcolor"] } siphasher = { version = "0.3", features = ["std"] } sized-chunks = { version = "0.6", features = ["std"] } slab = { version = "0.4", features = ["std"] } +slip10_ed25519 = { version = "0.1", default-features = false } slug = { version = "0.1", default-features = false } smallvec = { version = "1", default-features = false } smawk = { version = "0.3", default-features = false } @@ -523,6 +516,7 @@ thrift = { version = "0.15", default-features = false } time-c65f7effa3be6d31 = { package = "time", version = "0.1", default-features = false } time-468e82937335b1c9 = { package = "time", version = "0.3", features = ["alloc", "formatting", "itoa", "macros", "parsing", "std", "time-macros"] } tint = { version = "1", default-features = false } +tiny-bip39 = { version = "1", features = ["chinese-simplified", "chinese-traditional", "french", "italian", "japanese", "korean", "spanish"] } tinytemplate = { version = "1", default-features = false } tinyvec = { version = "1", features = ["alloc", "tinyvec_macros"] } tinyvec_macros = { version = "0.1", default-features = false } @@ -639,8 +633,7 @@ async-trait = { version = "0.1", default-features = false } atoi = { version = "1", default-features = false } atomicwrites = { version = "0.3", default-features = false } atty = { version = "0.2", default-features = false } -autocfg-c65f7effa3be6d31 = { package = "autocfg", version = "0.1", default-features = false } -autocfg-dff4ba8e3ae991db = { package = "autocfg", version = "1", default-features = false } +autocfg = { version = "1", default-features = false } axum = { version = "0.5", features = ["form", "http1", "json", "matched-path", "original-uri", "query", "serde_json", "serde_urlencoded", "tower-log"] } axum-core = { version = "0.2", default-features = false } backoff = { version = "0.4", features = ["futures", "futures-core", "pin-project-lite", "tokio", "tokio_1"] } @@ -655,11 +648,10 @@ better_typeid_derive = { version = "0.1", default-features = false } bimap = { version = "0.6", features = ["std"] } bincode = { version = "1", default-features = false } bindgen = { version = "0.60", default-features = false, features = ["runtime"] } -bip39 = { git = "https://github.com/patrickkuo/rust-bip39.git", rev = "a76fe8310416555e6383b42b8acc4eb93c7bcc89", features = ["rand", "serde", "std", "unicode-normalization"] } +bip32 = { version = "0.4", features = ["alloc", "bip39", "k256", "mnemonic", "once_cell", "pbkdf2", "secp256k1", "std"] } bit-set = { version = "0.5", features = ["std"] } bit-vec = { version = "0.6", default-features = false, features = ["std"] } -bitcoin_hashes-a6292c17cd707f01 = { package = "bitcoin_hashes", version = "0.11", default-features = false } -bitcoin_hashes-274715c4dabd11b0 = { package = "bitcoin_hashes", version = "0.9", features = ["std"] } +bitcoin_hashes = { version = "0.11", default-features = false } bitflags = { version = "1" } bitmaps = { version = "2", features = ["std"] } blake2 = { version = "0.9", features = ["std"] } @@ -669,7 +661,7 @@ block-buffer-274715c4dabd11b0 = { package = "block-buffer", version = "0.9", def block-padding = { version = "0.2", default-features = false } bls-crypto = { git = "https://github.com/huitseeker/celo-bls-snark-rs", branch = "updates-2", features = ["compat"] } blst = { version = "0.3" } -bs58 = { version = "0.4", features = ["alloc", "std"] } +bs58 = { version = "0.4", features = ["alloc", "check", "sha2", "std"] } bstr = { version = "0.2", features = ["lazy_static", "regex-automata", "serde", "serde1", "serde1-nostd", "std", "unicode"] } bulletproofs = { version = "4", features = ["rand", "std", "thiserror"] } bumpalo = { version = "3" } @@ -840,6 +832,7 @@ hex = { version = "0.4", features = ["alloc", "std"] } hex-literal = { version = "0.3", default-features = false } hkdf = { version = "0.12", default-features = false, features = ["std"] } hmac = { version = "0.12", default-features = false, features = ["reset", "std"] } +hmac-sha512 = { version = "0.1", features = ["sha384"] } home = { version = "0.5", default-features = false } http = { version = "0.2", default-features = false } http-body = { version = "0.4", default-features = false } @@ -1001,6 +994,7 @@ parking_lot_core-274715c4dabd11b0 = { package = "parking_lot_core", version = "0 parse-zoneinfo = { version = "0.3", default-features = false } paste = { version = "1", default-features = false } pathdiff = { version = "0.2", default-features = false, features = ["camino"] } +pbkdf2 = { version = "0.11", default-features = false } peeking_take_while = { version = "0.1", default-features = false } pem = { version = "1", default-features = false } percent-encoding = { version = "2", features = ["alloc"] } @@ -1056,23 +1050,13 @@ quinn-udp = { version = "0.1", default-features = false } quote-3b31131e45eafb45 = { package = "quote", version = "0.6", features = ["proc-macro"] } quote-dff4ba8e3ae991db = { package = "quote", version = "1", features = ["proc-macro"] } radix_trie = { version = "0.2", default-features = false } -rand-3b31131e45eafb45 = { package = "rand", version = "0.6", features = ["alloc", "rand_os", "std"] } rand-ca01ad9e24f5d932 = { package = "rand", version = "0.7", features = ["alloc", "getrandom", "getrandom_package", "libc", "std"] } rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["alloc", "getrandom", "libc", "rand_chacha", "small_rng", "std", "std_rng"] } -rand_chacha-c65f7effa3be6d31 = { package = "rand_chacha", version = "0.1", default-features = false } rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3", features = ["std"] } -rand_core-468e82937335b1c9 = { package = "rand_core", version = "0.3", default-features = false } -rand_core-9fbad63c4bcf4a8f = { package = "rand_core", version = "0.4", default-features = false, features = ["alloc", "std"] } rand_core-d8f496e17d97b5cb = { package = "rand_core", version = "0.5", default-features = false, features = ["alloc", "getrandom", "std"] } rand_core-3b31131e45eafb45 = { package = "rand_core", version = "0.6", default-features = false, features = ["alloc", "getrandom", "std"] } rand_distr = { version = "0.4", features = ["alloc", "std"] } -rand_hc = { version = "0.1", default-features = false } -rand_isaac = { version = "0.1", default-features = false } -rand_jitter = { version = "0.1", default-features = false, features = ["std"] } -rand_os = { version = "0.1", default-features = false } -rand_pcg = { version = "0.1", default-features = false } -rand_xorshift-c65f7effa3be6d31 = { package = "rand_xorshift", version = "0.1", default-features = false } -rand_xorshift-468e82937335b1c9 = { package = "rand_xorshift", version = "0.3", default-features = false } +rand_xorshift = { version = "0.3", default-features = false } rand_xoshiro = { version = "0.6", default-features = false } rayon = { version = "1", default-features = false } rayon-core = { version = "1", default-features = false } @@ -1090,6 +1074,7 @@ reqwest = { version = "0.11", features = ["__tls", "blocking", "default-tls", "h retain_mut = { version = "0.1", default-features = false } rfc6979 = { version = "0.3", default-features = false } ring = { version = "0.16", features = ["alloc", "dev_urandom_fallback", "once_cell"] } +ripemd = { version = "0.1", default-features = false } roaring = { version = "0.10", default-features = false } rocksdb = { version = "0.19", features = ["bzip2", "lz4", "multi-threaded-cf", "snappy", "zlib", "zstd"] } rust-ini = { version = "0.13", default-features = false } @@ -1154,6 +1139,7 @@ simplelog = { version = "0.9", features = ["termcolor"] } siphasher = { version = "0.3", features = ["std"] } sized-chunks = { version = "0.6", features = ["std"] } slab = { version = "0.4", features = ["std"] } +slip10_ed25519 = { version = "0.1", default-features = false } slug = { version = "0.1", default-features = false } smallvec = { version = "1", default-features = false } smawk = { version = "0.3", default-features = false } @@ -1210,6 +1196,7 @@ time-c65f7effa3be6d31 = { package = "time", version = "0.1", default-features = time-468e82937335b1c9 = { package = "time", version = "0.3", features = ["alloc", "formatting", "itoa", "macros", "parsing", "std", "time-macros"] } time-macros = { version = "0.2", default-features = false } tint = { version = "1", default-features = false } +tiny-bip39 = { version = "1", features = ["chinese-simplified", "chinese-traditional", "french", "italian", "japanese", "korean", "spanish"] } tinytemplate = { version = "1", default-features = false } tinyvec = { version = "1", features = ["alloc", "tinyvec_macros"] } tinyvec_macros = { version = "0.1", default-features = false } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b391aef74b215..fb83d1682b110 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,7 @@ importers: specifiers: '@mysten/sui.js': workspace:* '@reduxjs/toolkit': ^1.8.3 + '@scure/bip32': ^1.1.0 '@types/dotenv-webpack': ^7.0.3 '@types/node': ^18.0.6 '@types/react': ^18.0.15 @@ -248,6 +249,7 @@ importers: cross-env: ^7.0.3 css-loader: ^6.7.1 dotenv-webpack: ^8.0.0 + ed25519-hd-key: ^1.3.0 eslint: ^8.20.0 eslint-config-prettier: ^8.5.0 eslint-config-react-app: ^7.0.1 @@ -292,10 +294,12 @@ importers: dependencies: '@mysten/sui.js': link:../sdk/typescript '@reduxjs/toolkit': 1.8.5_kkwg4cbsojnjnupd3btipussee + '@scure/bip32': 1.1.0 bip39-light: 1.0.7 bootstrap-icons: 1.9.1 buffer: 6.0.3 classnames: 2.3.1 + ed25519-hd-key: 1.3.0 formik: 2.2.9_react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -3291,6 +3295,18 @@ packages: resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==} dev: true + /@scure/base/1.1.1: + resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + dev: false + + /@scure/bip32/1.1.0: + resolution: {integrity: sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==} + dependencies: + '@noble/hashes': 1.1.2 + '@noble/secp256k1': 1.6.3 + '@scure/base': 1.1.1 + dev: false + /@sentry/browser/7.11.1: resolution: {integrity: sha512-k2XHuzPfnm8VJPK5eWd1+Y5VCgN42sLveb8Qxc3prb5PSL416NWMLZaoB7RMIhy430fKrSFiosnm6QDk2M6pbA==} engines: {node: '>=8'} @@ -6717,6 +6733,13 @@ packages: safe-buffer: 5.2.1 dev: true + /ed25519-hd-key/1.3.0: + resolution: {integrity: sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg==} + dependencies: + create-hmac: 1.1.7 + tweetnacl: 1.0.3 + dev: false + /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true diff --git a/sdk/typescript/src/cryptography/ed25519-keypair.ts b/sdk/typescript/src/cryptography/ed25519-keypair.ts index 4aa36eef041ec..970f78bcd2374 100644 --- a/sdk/typescript/src/cryptography/ed25519-keypair.ts +++ b/sdk/typescript/src/cryptography/ed25519-keypair.ts @@ -53,8 +53,7 @@ export class Ed25519Keypair implements Keypair { * Create a Ed25519 keypair from a raw secret key byte array. * * This method should only be used to recreate a keypair from a previously - * generated secret key. Generating keypairs from a random seed should be done - * with the {@link Keypair.fromSeed} method. + * generated secret key. * * @throws error if the provided secret key is invalid and validation is not skipped. * @@ -101,4 +100,4 @@ export class Ed25519Keypair implements Keypair { nacl.sign.detached(data.getData(), this.keypair.secretKey) ); } -} +} \ No newline at end of file diff --git a/wallet/package.json b/wallet/package.json index 8f9ae1fbb46e5..a58fcd1cf8f4b 100644 --- a/wallet/package.json +++ b/wallet/package.json @@ -77,10 +77,12 @@ "dependencies": { "@mysten/sui.js": "workspace:*", "@reduxjs/toolkit": "^1.8.3", + "@scure/bip32": "^1.1.0", "bip39-light": "^1.0.7", "bootstrap-icons": "^1.9.1", "buffer": "^6.0.3", "classnames": "^2.3.1", + "ed25519-hd-key": "^1.3.0", "formik": "^2.2.9", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/wallet/src/shared/cryptography/mnemonics.test.ts b/wallet/src/shared/cryptography/mnemonics.test.ts index a60eaabfb15fe..ab70c5e527fe9 100644 --- a/wallet/src/shared/cryptography/mnemonics.test.ts +++ b/wallet/src/shared/cryptography/mnemonics.test.ts @@ -1,15 +1,22 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { Base64DataBuffer, Ed25519Keypair } from '@mysten/sui.js'; +import { + Base64DataBuffer, + Ed25519Keypair, + Secp256k1Keypair, +} from '@mysten/sui.js'; import { describe, it, expect } from 'vitest'; import { + deriveKeypairFromMnemonics, + deriveSecp256k1KeypairFromMnemonics, generateMnemonicsAndKeypair, getKeypairFromMnemonics, normalizeMnemonics, } from './mnemonics'; +// TODO(joyqvq): move this and its test file to sdk/typescript/cryptography describe('mnemonics', () => { it('generate mnemonics', () => { const [mnemonics, keypair] = generateMnemonicsAndKeypair(); @@ -39,4 +46,84 @@ describe('mnemonics', () => { 'xh7GF/bXW628rtrACyikHH0zos+gwHjDsUPkTadefZw=' ); }); + + it('invalid mnemonics to get keypair', () => { + expect(() => { + getKeypairFromMnemonics('aaa'); + }).toThrow('Invalid mnemonics'); + }); + + it('derive ed25519 keypair from path and mnemonics', () => { + // Test case generated against rust: /sui/crates/sui/src/unit_tests/keytool_tests.rs#L149 + const keypairData = deriveKeypairFromMnemonics( + `m/44'/784'/0'/0'/0'`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + const keypair = new Ed25519Keypair(keypairData); + + expect(keypair.getPublicKey().toBase64()).toEqual( + 'aFstb5h4TddjJJryHJL1iMob6AxAqYxVv3yRt05aweI=' + ); + expect(keypair.getPublicKey().toSuiAddress()).toEqual( + '1a4623343cd42be47d67314fce0ad042f3c82685' + ); + }); + + it('incorrect coin type node for ed25519 derivation path', () => { + expect(() => { + deriveKeypairFromMnemonics( + `m/44'/0'/0'/0'/0'`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + }).toThrow('Invalid derivation path'); + }); + + it('incorrect purpose node for ed25519 derivation path', () => { + expect(() => { + deriveKeypairFromMnemonics( + `m/54'/784'/0'/0'/0'`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + }).toThrow('Invalid derivation path'); + }); + + it('invalid mnemonics to derive ed25519 keypair', () => { + expect(() => { + deriveKeypairFromMnemonics(`m/44'/784'/0'/0'/0'`, 'aaa'); + }).toThrow('Invalid mnemonics'); + }); + + it('derive secp256k1 keypair from path and mnemonics', () => { + // Test case generated against rust: /sui/crates/sui/src/unit_tests/keytool_tests.rs#L149 + const keypairData = deriveSecp256k1KeypairFromMnemonics( + `m/54'/784'/0'/0/0`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + const keypair = new Secp256k1Keypair(keypairData); + + expect(keypair.getPublicKey().toBase64()).toEqual( + 'A+NxdDVYKrM9LjFdIem8ThlQCh/EyM3HOhU2WJF3SxMf' + ); + expect(keypair.getPublicKey().toSuiAddress()).toEqual( + 'ed17b3f435c03ff69c2cdc6d394932e68375f20f' + ); + }); + + it('incorrect purpose node for secp256k1 derivation path', () => { + expect(() => { + deriveSecp256k1KeypairFromMnemonics( + `m/44'/784'/0'/0'/0'`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + }).toThrow('Invalid derivation path'); + }); + + it('incorrect hardened path for secp256k1 key derivation', () => { + expect(() => { + deriveSecp256k1KeypairFromMnemonics( + `m/54'/784'/0'/0'/0'`, + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + ); + }).toThrow('Invalid derivation path'); + }); }); diff --git a/wallet/src/shared/cryptography/mnemonics.ts b/wallet/src/shared/cryptography/mnemonics.ts index b88bcdc6dfd92..5e67af8451c25 100644 --- a/wallet/src/shared/cryptography/mnemonics.ts +++ b/wallet/src/shared/cryptography/mnemonics.ts @@ -1,10 +1,12 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { HDKey } from '@scure/bip32'; import bip39 from 'bip39-light'; +import { derivePath, getPublicKey } from 'ed25519-hd-key'; import nacl from 'tweetnacl'; -import type { Ed25519KeypairData } from '@mysten/sui.js'; +import type { Ed25519KeypairData, Secp256k1KeypairData } from '@mysten/sui.js'; /** * Generate a 12-word random mnemonic and keypair using crypto.randomBytes @@ -22,12 +24,16 @@ export function generateMnemonic(): string { } /** - * Derive public key and private key from the Mnemonics + * Get public key and private key from the Mnemonics * @param mnemonics a 12-word seed phrase * @returns public key and private key */ export function getKeypairFromMnemonics(mnemonics: string): Ed25519KeypairData { - const seed = bip39.mnemonicToSeed(normalizeMnemonics(mnemonics)); + const normalized = normalizeMnemonics(mnemonics); + if (!validateMnemonics(normalized)) { + throw new Error('Invalid mnemonics'); + } + const seed = bip39.mnemonicToSeed(normalized); return nacl.sign.keyPair.fromSeed( // keyPair.fromSeed only takes a 32-byte array where `seed` is a 64-byte array new Uint8Array(seed.toJSON().data.slice(0, 32)) @@ -48,4 +54,84 @@ export function normalizeMnemonics(mnemonics: string): string { .join(' '); } +/** + * Parse and validate a path that is compliant to SLIP-0010 in form m/44'/784'/{account_index}'/{change_index}'/{address_index}' + * + * @param path path string (e.g. `m/44'/784'/0'/0'/0'`) + */ +export function isValidHardenedPath(path: string): boolean { + if (!new RegExp("^m\\/44'\\/784'\\/0'\\/[0-9]+'\\/[0-9]+'+$").test(path)) { + return false; + } + return true; +} + +/** + * Derive Ed25519 public key and private key from the Mnemonics using SLIP-0010 harden derivation path. + * @param mnemonics a 12-word seed phrase + * @param path path string (`m/44'/784'/0'/0'/0'`) + * @returns public key and private key + */ +export function deriveKeypairFromMnemonics( + path: string, + mnemonics: string +): Ed25519KeypairData { + if (!isValidHardenedPath(path)) { + throw new Error('Invalid derivation path'); + } + + const normalized = normalizeMnemonics(mnemonics); + if (!validateMnemonics(normalized)) { + throw new Error('Invalid mnemonics'); + } + + const { key } = derivePath(path, bip39.mnemonicToSeedHex(normalized)); + + return { publicKey: getPublicKey(key), secretKey: key }; +} + +/** + * Parse and validate a path that is compliant to BIP-32 in form m/54'/784'/{account_index}'/{change_index}/{address_index} + * Note that the purpose for Secp256k1 is registered as 54, to differentiate from Ed25519 with purpose 44. + * + * @param path path string (e.g. `m/54'/784'/0'/0/0`) + */ +export function isValidBIP32Path(path: string): boolean { + if ( + !new RegExp("^m\\/54'\\/784'\\/[0-9]+'\\/[0-9]+\\/[0-9]+$").test(path) + ) { + return false; + } + return true; +} + +/** + * Derive Secp256k1 public key and private key from the Mnemonics using BIP32 derivation path. + * @param mnemonics a 12-word seed phrase + * @param path path string (`m/54'/784'/1'/0/0`) + * @returns public key and private key + */ +export function deriveSecp256k1KeypairFromMnemonics( + path: string, + mnemonics: string +): Secp256k1KeypairData { + if (!isValidBIP32Path(path)) { + throw new Error('Invalid derivation path'); + } + + const normalized = normalizeMnemonics(mnemonics); + if (!validateMnemonics(normalized)) { + throw new Error('Invalid mnemonics'); + } + const key = HDKey.fromMasterSeed(bip39.mnemonicToSeed(normalized)).derive( + path + ); + + if (key.privateKey === null || key.publicKey === null) { + throw new Error('Invalid derivation path'); + } + + return { publicKey: key.publicKey, secretKey: key.privateKey }; +} + export const validateMnemonics = bip39.validateMnemonic; diff --git a/wallet/src/types/bip39-light.d.ts b/wallet/src/types/bip39-light.d.ts index fb8791793e2aa..40b59e508967a 100644 --- a/wallet/src/types/bip39-light.d.ts +++ b/wallet/src/types/bip39-light.d.ts @@ -13,4 +13,8 @@ declare module 'bip39-light' { ): string; export function mnemonicToSeed(mnemonic: string, password?: string): Buffer; export function validateMnemonic(mnemonic: string): boolean; + export function mnemonicToSeedHex( + mnemonic: string, + password?: string + ): string; }