From a3983f5a89b580fe7873b9fec98713ef5eb828b1 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:47:34 +0100 Subject: [PATCH] fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: --- Cargo.lock | 1 + .../configs/debug/wallet/wallet_config.json | 545 ++++++++++++++++++ integration_tests/src/lib.rs | 2 +- integration_tests/tests/wallet_ffi.rs | 5 +- key_protocol/src/key_management/mod.rs | 9 +- .../src/key_management/secret_holders.rs | 83 ++- key_protocol/src/key_protocol_core/mod.rs | 5 +- wallet-ffi/src/wallet.rs | 2 +- wallet-ffi/wallet_ffi.h | 153 +++++ wallet/Cargo.toml | 1 + wallet/src/chain_storage.rs | 41 +- wallet/src/cli/mod.rs | 16 +- wallet/src/lib.rs | 28 +- wallet/src/main.rs | 10 +- 14 files changed, 866 insertions(+), 35 deletions(-) create mode 100644 integration_tests/configs/debug/wallet/wallet_config.json diff --git a/Cargo.lock b/Cargo.lock index 62bf5f9c..632c7445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9026,6 +9026,7 @@ dependencies = [ "anyhow", "async-stream", "base64 0.22.1", + "bip39", "borsh", "bytemuck", "clap", diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json new file mode 100644 index 00000000..359e9eb2 --- /dev/null +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -0,0 +1,545 @@ +{ + "sequencer_addr": "http://127.0.0.1:3040", + "seq_poll_timeout_millis": 12000, + "seq_tx_poll_max_blocks": 5, + "seq_poll_max_retries": 5, + "seq_block_poll_max_amount": 100, + "initial_accounts": [ + { + "Public": { + "account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "pub_sign_key": [ + 16, + 162, + 106, + 154, + 236, + 125, + 52, + 184, + 35, + 100, + 238, + 174, + 69, + 197, + 41, + 77, + 187, + 10, + 118, + 75, + 0, + 11, + 148, + 238, + 185, + 181, + 133, + 17, + 220, + 72, + 124, + 77 + ] + } + }, + { + "Public": { + "account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "pub_sign_key": [ + 113, + 121, + 64, + 177, + 204, + 85, + 229, + 214, + 178, + 6, + 109, + 191, + 29, + 154, + 63, + 38, + 242, + 18, + 244, + 219, + 8, + 208, + 35, + 136, + 23, + 127, + 207, + 237, + 216, + 169, + 190, + 27 + ] + } + }, + { + "Private": { + "account_id": "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw", + "account": { + "program_owner": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "balance": 10000, + "data": [], + "nonce": 0 + }, + "key_chain": { + "secret_spending_key": [ + 251, + 82, + 235, + 1, + 146, + 96, + 30, + 81, + 162, + 234, + 33, + 15, + 123, + 129, + 116, + 0, + 84, + 136, + 176, + 70, + 190, + 224, + 161, + 54, + 134, + 142, + 154, + 1, + 18, + 251, + 242, + 189 + ], + "private_key_holder": { + "nullifier_secret_key": [ + 29, + 250, + 10, + 187, + 35, + 123, + 180, + 250, + 246, + 97, + 216, + 153, + 44, + 156, + 16, + 93, + 241, + 26, + 174, + 219, + 72, + 84, + 34, + 247, + 112, + 101, + 217, + 243, + 189, + 173, + 75, + 20 + ], + "incoming_viewing_secret_key": [ + 251, + 201, + 22, + 154, + 100, + 165, + 218, + 108, + 163, + 190, + 135, + 91, + 145, + 84, + 69, + 241, + 46, + 117, + 217, + 110, + 197, + 248, + 91, + 193, + 14, + 104, + 88, + 103, + 67, + 153, + 182, + 158 + ], + "outgoing_viewing_secret_key": [ + 25, + 67, + 121, + 76, + 175, + 100, + 30, + 198, + 105, + 123, + 49, + 169, + 75, + 178, + 75, + 210, + 100, + 143, + 210, + 243, + 228, + 243, + 21, + 18, + 36, + 84, + 164, + 186, + 139, + 113, + 214, + 12 + ] + }, + "nullifer_public_key": [ + 63, + 202, + 178, + 231, + 183, + 82, + 237, + 212, + 216, + 221, + 215, + 255, + 153, + 101, + 177, + 161, + 254, + 210, + 128, + 122, + 54, + 190, + 230, + 151, + 183, + 64, + 225, + 229, + 113, + 1, + 228, + 97 + ], + "incoming_viewing_public_key": [ + 3, + 235, + 139, + 131, + 237, + 177, + 122, + 189, + 6, + 177, + 167, + 178, + 202, + 117, + 246, + 58, + 28, + 65, + 132, + 79, + 220, + 139, + 119, + 243, + 187, + 160, + 212, + 121, + 61, + 247, + 116, + 72, + 205 + ] + } + } + }, + { + "Private": { + "account_id": "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX", + "account": { + "program_owner": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "balance": 20000, + "data": [], + "nonce": 0 + }, + "key_chain": { + "secret_spending_key": [ + 238, + 171, + 241, + 69, + 111, + 217, + 85, + 64, + 19, + 82, + 18, + 189, + 32, + 91, + 78, + 175, + 107, + 7, + 109, + 60, + 52, + 44, + 243, + 230, + 72, + 244, + 192, + 92, + 137, + 33, + 118, + 254 + ], + "private_key_holder": { + "nullifier_secret_key": [ + 25, + 211, + 215, + 119, + 57, + 223, + 247, + 37, + 245, + 144, + 122, + 29, + 118, + 245, + 83, + 228, + 23, + 9, + 101, + 120, + 88, + 33, + 238, + 207, + 128, + 61, + 110, + 2, + 89, + 62, + 164, + 13 + ], + "incoming_viewing_secret_key": [ + 193, + 181, + 14, + 196, + 142, + 84, + 15, + 65, + 128, + 101, + 70, + 196, + 241, + 47, + 130, + 221, + 23, + 146, + 161, + 237, + 221, + 40, + 19, + 126, + 59, + 15, + 169, + 236, + 25, + 105, + 104, + 231 + ], + "outgoing_viewing_secret_key": [ + 20, + 170, + 220, + 108, + 41, + 23, + 155, + 217, + 247, + 190, + 175, + 168, + 247, + 34, + 105, + 134, + 114, + 74, + 104, + 91, + 211, + 62, + 126, + 13, + 130, + 100, + 241, + 214, + 250, + 236, + 38, + 150 + ] + }, + "nullifer_public_key": [ + 192, + 251, + 166, + 243, + 167, + 236, + 84, + 249, + 35, + 136, + 130, + 172, + 219, + 225, + 161, + 139, + 229, + 89, + 243, + 125, + 194, + 213, + 209, + 30, + 23, + 174, + 100, + 244, + 124, + 74, + 140, + 47 + ], + "incoming_viewing_public_key": [ + 2, + 181, + 98, + 93, + 216, + 241, + 241, + 110, + 58, + 198, + 119, + 174, + 250, + 184, + 1, + 204, + 200, + 173, + 44, + 238, + 37, + 247, + 170, + 156, + 100, + 254, + 116, + 242, + 28, + 183, + 187, + 77, + 255 + ] + } + } + } + ] +} \ No newline at end of file diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 6497698d..fb25c0aa 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -218,7 +218,7 @@ impl TestContext { let config_overrides = WalletConfigOverrides::default(); let wallet_password = "test_pass".to_owned(); - let wallet = WalletCore::new_init_storage( + let (wallet, _mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index e57e6b13..092b7157 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -205,13 +205,14 @@ fn new_wallet_rust_with_default_config(password: &str) -> WalletCore { let config_path = tempdir.path().join("wallet_config.json"); let storage_path = tempdir.path().join("storage.json"); - WalletCore::new_init_storage( + let (core, _mnemonic) = WalletCore::new_init_storage( config_path.to_path_buf(), storage_path.to_path_buf(), None, password.to_string(), ) - .unwrap() + .unwrap(); + core } fn load_existing_ffi_wallet(home: &Path) -> *mut WalletHandle { diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index d46dcf35..5f58b43e 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -40,10 +40,10 @@ impl KeyChain { } } - pub fn new_mnemonic(passphrase: String) -> Self { + pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) { // Currently dropping SeedHolder at the end of initialization. // Not entirely sure if we need it in the future. - let seed_holder = SeedHolder::new_mnemonic(passphrase); + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); let secret_spending_key = seed_holder.produce_top_secret_key_holder(); let private_key_holder = secret_spending_key.produce_private_key_holder(None); @@ -51,12 +51,13 @@ impl KeyChain { let nullifer_public_key = private_key_holder.generate_nullifier_public_key(); let viewing_public_key = private_key_holder.generate_viewing_public_key(); - Self { + (Self { secret_spending_key, private_key_holder, nullifer_public_key, viewing_public_key, - } + }, + mnemonic) } pub fn calculate_shared_secret_receiver( diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index d5aac258..954931e0 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -8,8 +8,6 @@ use rand::{RngCore, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest, digest::FixedOutput}; -const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32]; - #[derive(Debug)] /// Seed holder. Non-clonable to ensure that different holders use different seeds. /// Produces `TopSecretKeyHolder` objects. @@ -46,9 +44,23 @@ impl SeedHolder { } } - pub fn new_mnemonic(passphrase: String) -> Self { - let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) - .expect("Enthropy must be a multiple of 32 bytes"); + pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) { + let mut entropy_bytes: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut entropy_bytes); + + let mnemonic = + Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes"); + let seed_wide = mnemonic.to_seed(passphrase); + + ( + Self { + seed: seed_wide.to_vec(), + }, + mnemonic, + ) + } + + pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self { let seed_wide = mnemonic.to_seed(passphrase); Self { @@ -163,12 +175,63 @@ mod tests { } #[test] - fn two_seeds_generated_same_from_same_mnemonic() { - let mnemonic = "test_pass"; + fn two_seeds_recovered_same_from_same_mnemonic() { + let passphrase = "test_pass"; - let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_string()); - let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_string()); + // Generate a mnemonic with random entropy + let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); - assert_eq!(seed_holder1.seed, seed_holder2.seed); + // Recover from the same mnemonic + let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase); + + assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed); + } + + #[test] + fn new_mnemonic_generates_different_seeds_each_time() { + let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic(""); + let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic(""); + + // Different entropy should produce different mnemonics and seeds + assert_ne!(mnemonic1.to_string(), mnemonic2.to_string()); + assert_ne!(seed_holder1.seed, seed_holder2.seed); + } + + #[test] + fn new_mnemonic_generates_24_word_phrase() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + // 256 bits of entropy produces a 24-word mnemonic + let word_count = mnemonic.to_string().split_whitespace().count(); + assert_eq!(word_count, 24); + } + + #[test] + fn new_mnemonic_produces_valid_seed_length() { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); + + assert_eq!(seed_holder.seed.len(), 64); + } + + #[test] + fn different_passphrases_produce_different_seeds() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a"); + let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b"); + + // Same mnemonic but different passphrases should produce different seeds + assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed); + } + + #[test] + fn empty_passphrase_is_deterministic() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed1 = SeedHolder::from_mnemonic(&mnemonic, ""); + let seed2 = SeedHolder::from_mnemonic(&mnemonic, ""); + + // Same mnemonic and passphrase should always produce the same seed + assert_eq!(seed1.seed, seed2.seed); } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 42e4b672..f0f71657 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -187,11 +187,12 @@ impl NSSAUserData { impl Default for NSSAUserData { fn default() -> Self { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); Self::new_with_accounts( BTreeMap::new(), BTreeMap::new(), - KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_string())), - KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_string())), + KeyTreePublic::new(&seed_holder), + KeyTreePrivate::new(&seed_holder), ) .unwrap() } diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs index 7dd76d4e..1144f811 100644 --- a/wallet-ffi/src/wallet.rs +++ b/wallet-ffi/src/wallet.rs @@ -115,7 +115,7 @@ pub unsafe extern "C" fn wallet_ffi_create_new( }; match WalletCore::new_init_storage(config_path, storage_path, None, password) { - Ok(core) => { + Ok((core, _mnemonic)) => { let wrapper = Box::new(WalletWrapper { core: Mutex::new(core), }); diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 0b2b0176..d909e14b 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -344,6 +344,30 @@ enum WalletFfiError wallet_ffi_get_account_public(struct WalletHandle *handle, const struct FfiBytes32 *account_id, struct FfiAccount *out_account); +/** + * Get full private account data from the local storage. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: The account ID (32 bytes) + * - `out_account`: Output pointer for account data + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Memory + * The account data must be freed with `wallet_ffi_free_account_data()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_account` must be a valid pointer to a `FfiAccount` struct + */ +enum WalletFfiError wallet_ffi_get_account_private(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiAccount *out_account); + /** * Free account data returned by `wallet_ffi_get_account_public`. * @@ -546,6 +570,108 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle, const uint8_t (*amount)[16], struct FfiTransferResult *out_result); +/** + * Send a shielded token transfer. + * + * Transfers tokens from a public account to a private account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source account ID (must be owned by this wallet) + * - `to_keys`: Destination account keys + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiPrivateAccountKeys *to_keys, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Send a deshielded token transfer. + * + * Transfers tokens from a private account to a public account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source account ID (must be owned by this wallet) + * - `to`: Destination account ID + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiBytes32 *to, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Send a private token transfer. + * + * Transfers tokens from a private account to another private account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source account ID (must be owned by this wallet) + * - `to_keys`: Destination account keys + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_private(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiPrivateAccountKeys *to_keys, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + /** * Register a public account on the network. * @@ -573,6 +699,33 @@ enum WalletFfiError wallet_ffi_register_public_account(struct WalletHandle *hand const struct FfiBytes32 *account_id, struct FfiTransferResult *out_result); +/** + * Register a private account on the network. + * + * This initializes a private account. The account must be + * owned by this wallet. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: Account ID to register + * - `out_result`: Output pointer for registration result + * + * # Returns + * - `Success` if the registration was submitted successfully + * - Error code on failure + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_register_private_account(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiTransferResult *out_result); + /** * Free a transfer result returned by `wallet_ffi_transfer_public` or * `wallet_ffi_register_public_account`. diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 15a1046d..38a44cbb 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -11,6 +11,7 @@ common.workspace = true key_protocol.workspace = true token_core.workspace = true amm_core.workspace = true +bip39.workspace = true anyhow.workspace = true serde_json.workspace = true diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 2b3f3f4e..8d822924 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashMap, btree_map::Entry}; use anyhow::Result; +use bip39::Mnemonic; use key_protocol::{ key_management::{ key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, @@ -91,7 +92,7 @@ impl WalletChainStore { }) } - pub fn new_storage(config: WalletConfig, password: String) -> Result { + pub fn new_storage(config: WalletConfig, password: String) -> Result<(Self, Mnemonic)> { let mut public_init_acc_map = BTreeMap::new(); let mut private_init_acc_map = BTreeMap::new(); @@ -111,13 +112,43 @@ impl WalletChainStore { } } - let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); - let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); + // TODO: Use password for storage encryption + let _ = password; + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); + + Ok(( + Self { + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, + wallet_config: config, + labels: HashMap::new(), + }, + mnemonic, + )) + } + + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage( + config: WalletConfig, + mnemonic: &Mnemonic, + password: &str, + ) -> Result { + // TODO: Use password for storage encryption + let _ = password; + let seed_holder = SeedHolder::from_mnemonic(mnemonic, ""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); Ok(Self { user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, + BTreeMap::new(), + BTreeMap::new(), public_tree, private_tree, )?, diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index afd313a8..f623abba 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,6 +1,7 @@ -use std::{io::Write, path::PathBuf}; +use std::{io::Write, path::PathBuf, str::FromStr}; use anyhow::{Context, Result}; +use bip39::Mnemonic; use clap::{Parser, Subcommand}; use common::HashType; use nssa::{ProgramDeploymentTransaction, program::Program}; @@ -150,8 +151,9 @@ pub async fn execute_subcommand( config_subcommand.handle_subcommand(wallet_core).await? } Command::RestoreKeys { depth } => { + let mnemonic = read_mnemonic_from_stdin()?; let password = read_password_from_stdin()?; - wallet_core.reset_storage(password)?; + wallet_core.restore_storage(&mnemonic, &password)?; execute_keys_restoration(wallet_core, depth).await?; SubcommandReturnValue::Empty @@ -202,6 +204,16 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_string()) } +pub fn read_mnemonic_from_stdin() -> Result { + let mut phrase = String::new(); + + print!("Input recovery phrase: "); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut phrase)?; + + Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase") +} + pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> { wallet_core .storage diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index cb364fd3..1eb8b150 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::{Context, Result}; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use bip39::Mnemonic; use chain_storage::WalletChainStore; use common::{ HashType, error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse, @@ -87,14 +88,23 @@ impl WalletCore { storage_path: PathBuf, config_overrides: Option, password: String, - ) -> Result { - Self::new( + ) -> Result<(Self, Mnemonic)> { + let mut mnemonic_out = None; + let wallet = Self::new( config_path, storage_path, config_overrides, - |config| WalletChainStore::new_storage(config, password), + |config| { + let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?; + mnemonic_out = Some(mnemonic); + Ok(storage) + }, 0, - ) + )?; + Ok(( + wallet, + mnemonic_out.expect("mnemonic should be set after new_storage"), + )) } fn new( @@ -139,9 +149,13 @@ impl WalletCore { &self.storage } - /// Reset storage - pub fn reset_storage(&mut self, password: String) -> Result<()> { - self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?; + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> { + self.storage = WalletChainStore::restore_storage( + self.storage.wallet_config.clone(), + mnemonic, + password, + )?; Ok(()) } diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 045b1b83..1e71af5c 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -39,13 +39,21 @@ async fn main() -> Result<()> { println!("Persistent storage not found, need to execute setup"); let password = read_password_from_stdin()?; - let wallet = WalletCore::new_init_storage( + let (wallet, mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), password, )?; + println!(); + println!("IMPORTANT: Write down your recovery phrase and store it securely."); + println!("This is the only way to recover your wallet if you lose access."); + println!(); + println!("Recovery phrase:"); + println!(" {}", mnemonic); + println!(); + wallet.store_persistent_data().await?; wallet } else {