mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-03-26 20:23:45 +00:00
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: <password>
This commit is contained in:
parent
fb083ce91e
commit
f264d159fb
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -8650,6 +8650,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"base58",
|
"base58",
|
||||||
|
"bip39",
|
||||||
"clap",
|
"clap",
|
||||||
"common",
|
"common",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
545
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
545
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -256,11 +256,11 @@ impl TestContext {
|
|||||||
let config_overrides = WalletConfigOverrides::default();
|
let config_overrides = WalletConfigOverrides::default();
|
||||||
|
|
||||||
let wallet_password = "test_pass".to_owned();
|
let wallet_password = "test_pass".to_owned();
|
||||||
let wallet = WalletCore::new_init_storage(
|
let (wallet, _mnemonic) = WalletCore::new_init_storage(
|
||||||
config_path,
|
config_path,
|
||||||
storage_path,
|
storage_path,
|
||||||
Some(config_overrides),
|
Some(config_overrides),
|
||||||
wallet_password.clone(),
|
&wallet_password,
|
||||||
)
|
)
|
||||||
.context("Failed to init wallet")?;
|
.context("Failed to init wallet")?;
|
||||||
wallet
|
wallet
|
||||||
|
|||||||
@ -216,7 +216,9 @@ fn new_wallet_rust_with_default_config(password: &str) -> Result<WalletCore> {
|
|||||||
let config_path = tempdir.path().join("wallet_config.json");
|
let config_path = tempdir.path().join("wallet_config.json");
|
||||||
let storage_path = tempdir.path().join("storage.json");
|
let storage_path = tempdir.path().join("storage.json");
|
||||||
|
|
||||||
WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned())
|
let (core, _mnemonic) =
|
||||||
|
WalletCore::new_init_storage(config_path, storage_path, None, password)?;
|
||||||
|
Ok(core)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
|
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
|
||||||
|
|||||||
@ -42,10 +42,10 @@ impl KeyChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
|
||||||
// Currently dropping SeedHolder at the end of initialization.
|
// Currently dropping SeedHolder at the end of initialization.
|
||||||
// Not entirely sure if we need it in the future.
|
// 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 secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||||
|
|
||||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||||
@ -53,12 +53,15 @@ impl KeyChain {
|
|||||||
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
|
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
|
||||||
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
||||||
|
|
||||||
Self {
|
(
|
||||||
secret_spending_key,
|
Self {
|
||||||
private_key_holder,
|
secret_spending_key,
|
||||||
nullifier_public_key,
|
private_key_holder,
|
||||||
viewing_public_key,
|
nullifier_public_key,
|
||||||
}
|
viewing_public_key,
|
||||||
|
},
|
||||||
|
mnemonic,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||||
|
|
||||||
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
|
|
||||||
|
|
||||||
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||||
/// Produces `TopSecretKeyHolder` objects.
|
/// Produces `TopSecretKeyHolder` objects.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -49,9 +47,24 @@ impl SeedHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) {
|
||||||
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
|
let mut entropy_bytes: [u8; 32] = [0; 32];
|
||||||
.expect("Enthropy must be a multiple of 32 bytes");
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self {
|
||||||
let seed_wide = mnemonic.to_seed(passphrase);
|
let seed_wide = mnemonic.to_seed(passphrase);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -176,12 +189,63 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_seeds_generated_same_from_same_mnemonic() {
|
fn two_seeds_recovered_same_from_same_mnemonic() {
|
||||||
let mnemonic = "test_pass";
|
let passphrase = "test_pass";
|
||||||
|
|
||||||
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned());
|
// Generate a mnemonic with random entropy
|
||||||
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,11 +181,12 @@ impl NSSAUserData {
|
|||||||
|
|
||||||
impl Default for NSSAUserData {
|
impl Default for NSSAUserData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
||||||
Self::new_with_accounts(
|
Self::new_with_accounts(
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())),
|
KeyTreePublic::new(&seed_holder),
|
||||||
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())),
|
KeyTreePrivate::new(&seed_holder),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,8 +111,8 @@ pub unsafe extern "C" fn wallet_ffi_create_new(
|
|||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
};
|
};
|
||||||
|
|
||||||
match WalletCore::new_init_storage(config_path, storage_path, None, password) {
|
match WalletCore::new_init_storage(config_path, storage_path, None, &password) {
|
||||||
Ok(core) => {
|
Ok((core, _mnemonic)) => {
|
||||||
let wrapper = Box::new(WalletWrapper {
|
let wrapper = Box::new(WalletWrapper {
|
||||||
core: Mutex::new(core),
|
core: Mutex::new(core),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@ key_protocol.workspace = true
|
|||||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||||
token_core.workspace = true
|
token_core.workspace = true
|
||||||
amm_core.workspace = true
|
amm_core.workspace = true
|
||||||
|
bip39.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, btree_map::Entry};
|
use std::collections::{BTreeMap, HashMap, btree_map::Entry};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use bip39::Mnemonic;
|
||||||
use key_protocol::{
|
use key_protocol::{
|
||||||
key_management::{
|
key_management::{
|
||||||
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
||||||
@ -95,7 +96,7 @@ impl WalletChainStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_storage(config: WalletConfig, password: String) -> Result<Self> {
|
pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> {
|
||||||
let mut public_init_acc_map = BTreeMap::new();
|
let mut public_init_acc_map = BTreeMap::new();
|
||||||
let mut private_init_acc_map = BTreeMap::new();
|
let mut private_init_acc_map = BTreeMap::new();
|
||||||
|
|
||||||
@ -115,13 +116,43 @@ impl WalletChainStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone()));
|
// TODO: Use password for storage encryption
|
||||||
let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password));
|
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<Self> {
|
||||||
|
// 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 {
|
Ok(Self {
|
||||||
user_data: NSSAUserData::new_with_accounts(
|
user_data: NSSAUserData::new_with_accounts(
|
||||||
public_init_acc_map,
|
BTreeMap::new(),
|
||||||
private_init_acc_map,
|
BTreeMap::new(),
|
||||||
public_tree,
|
public_tree,
|
||||||
private_tree,
|
private_tree,
|
||||||
)?,
|
)?,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use std::{io::Write as _, path::PathBuf};
|
use std::{io::Write as _, path::PathBuf, str::FromStr as _};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
use bip39::Mnemonic;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use common::{HashType, transaction::NSSATransaction};
|
use common::{HashType, transaction::NSSATransaction};
|
||||||
use futures::TryFutureExt as _;
|
use futures::TryFutureExt as _;
|
||||||
@ -162,8 +163,9 @@ pub async fn execute_subcommand(
|
|||||||
config_subcommand.handle_subcommand(wallet_core).await?
|
config_subcommand.handle_subcommand(wallet_core).await?
|
||||||
}
|
}
|
||||||
Command::RestoreKeys { depth } => {
|
Command::RestoreKeys { depth } => {
|
||||||
|
let mnemonic = read_mnemonic_from_stdin()?;
|
||||||
let password = read_password_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?;
|
execute_keys_restoration(wallet_core, depth).await?;
|
||||||
|
|
||||||
SubcommandReturnValue::Empty
|
SubcommandReturnValue::Empty
|
||||||
@ -207,6 +209,16 @@ pub fn read_password_from_stdin() -> Result<String> {
|
|||||||
Ok(password.trim().to_owned())
|
Ok(password.trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
|
||||||
|
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<()> {
|
pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> {
|
||||||
wallet_core
|
wallet_core
|
||||||
.storage
|
.storage
|
||||||
|
|||||||
@ -222,10 +222,10 @@ impl Default for WalletConfig {
|
|||||||
let public_key2 = nssa::PublicKey::new_from_private_key(&pub_sign_key2);
|
let public_key2 = nssa::PublicKey::new_from_private_key(&pub_sign_key2);
|
||||||
let public_account_id2 = nssa::AccountId::from(&public_key2);
|
let public_account_id2 = nssa::AccountId::from(&public_key2);
|
||||||
|
|
||||||
let key_chain1 = KeyChain::new_mnemonic("default_private_account_1".to_owned());
|
let (key_chain1, _) = KeyChain::new_mnemonic("default_private_account_1");
|
||||||
let private_account_id1 = nssa::AccountId::from(&key_chain1.nullifier_public_key);
|
let private_account_id1 = nssa::AccountId::from(&key_chain1.nullifier_public_key);
|
||||||
|
|
||||||
let key_chain2 = KeyChain::new_mnemonic("default_private_account_2".to_owned());
|
let (key_chain2, _) = KeyChain::new_mnemonic("default_private_account_2");
|
||||||
let private_account_id2 = nssa::AccountId::from(&key_chain2.nullifier_public_key);
|
let private_account_id2 = nssa::AccountId::from(&key_chain2.nullifier_public_key);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
use bip39::Mnemonic;
|
||||||
use chain_storage::WalletChainStore;
|
use chain_storage::WalletChainStore;
|
||||||
use common::{HashType, transaction::NSSATransaction};
|
use common::{HashType, transaction::NSSATransaction};
|
||||||
use config::WalletConfig;
|
use config::WalletConfig;
|
||||||
@ -115,15 +116,24 @@ impl WalletCore {
|
|||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
storage_path: PathBuf,
|
storage_path: PathBuf,
|
||||||
config_overrides: Option<WalletConfigOverrides>,
|
config_overrides: Option<WalletConfigOverrides>,
|
||||||
password: String,
|
password: &str,
|
||||||
) -> Result<Self> {
|
) -> Result<(Self, Mnemonic)> {
|
||||||
Self::new(
|
let mut mnemonic_out = None;
|
||||||
|
let wallet = Self::new(
|
||||||
config_path,
|
config_path,
|
||||||
storage_path,
|
storage_path,
|
||||||
config_overrides,
|
config_overrides,
|
||||||
|config| WalletChainStore::new_storage(config, password),
|
|config| {
|
||||||
|
let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?;
|
||||||
|
mnemonic_out = Some(mnemonic);
|
||||||
|
Ok(storage)
|
||||||
|
},
|
||||||
0,
|
0,
|
||||||
)
|
)?;
|
||||||
|
Ok((
|
||||||
|
wallet,
|
||||||
|
mnemonic_out.expect("mnemonic should be set after new_storage"),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
@ -189,9 +199,13 @@ impl WalletCore {
|
|||||||
&self.storage
|
&self.storage
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset storage.
|
/// Restore storage from an existing mnemonic phrase.
|
||||||
pub fn reset_storage(&mut self, password: String) -> Result<()> {
|
pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> {
|
||||||
self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?;
|
self.storage = WalletChainStore::restore_storage(
|
||||||
|
self.storage.wallet_config.clone(),
|
||||||
|
mnemonic,
|
||||||
|
password,
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,13 +46,21 @@ async fn main() -> Result<()> {
|
|||||||
println!("Persistent storage not found, need to execute setup");
|
println!("Persistent storage not found, need to execute setup");
|
||||||
|
|
||||||
let password = read_password_from_stdin()?;
|
let password = read_password_from_stdin()?;
|
||||||
let wallet = WalletCore::new_init_storage(
|
let (wallet, mnemonic) = WalletCore::new_init_storage(
|
||||||
config_path,
|
config_path,
|
||||||
storage_path,
|
storage_path,
|
||||||
Some(config_overrides),
|
Some(config_overrides),
|
||||||
password,
|
&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.store_persistent_data().await?;
|
||||||
wallet
|
wallet
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user