Skip to content

Commit 19718b0

Browse files
committed
refactor(platform-wallet): build resolved master keys natively
`seed_for_mnemonic` and `master_key_for_mnemonic` turn a recovery phrase into its BIP-39 seed and its BIP-32 master key, with the seed in a zeroizing buffer. `wallet_id_for_mnemonic` and `first_receive_address_for_mnemonic` use them. The FFI's resolver helpers now only call the host's mnemonic resolver, check its buffer and UTF-8, and hand the phrase to these functions. So every resolver caller, including the address key reveals, gets its seed and master key from `platform-wallet`. Result codes and the retry classification are unchanged.
1 parent 53b5980 commit 19718b0

3 files changed

Lines changed: 43 additions & 20 deletions

File tree

‎packages/rs-platform-wallet-ffi/src/identity_keys_from_mnemonic.rs‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use key_wallet::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPu
77
use key_wallet::dip9::{
88
IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET,
99
};
10-
use platform_wallet::parse_mnemonic_any_language;
10+
use platform_wallet::{
11+
master_key_for_mnemonic, parse_mnemonic_any_language, seed_for_mnemonic, PlatformWalletError,
12+
};
1113
use zeroize::Zeroizing;
1214

1315
use crate::error::*;
@@ -135,14 +137,8 @@ pub(crate) unsafe fn resolve_master_from_resolver_classified(
135137
wallet_id: &[u8; 32],
136138
network: Network,
137139
) -> Result<ExtendedPrivKey, ResolveFailure> {
138-
let seed = resolve_seed_from_resolver_classified(mnemonic_resolver_handle, wallet_id)?;
139-
// A seed that cannot yield a master key is the stored material being
140-
// wrong, not the moment being wrong.
141-
ExtendedPrivKey::new_master(network, seed.as_ref()).map_err(|e| {
142-
ResolveFailure::permanent(PlatformWalletFFIResult::err(
143-
PlatformWalletFFIResultCode::ErrorWalletOperation,
144-
format!("failed to build master xpriv from resolved mnemonic: {e}"),
145-
))
140+
with_resolved_mnemonic(mnemonic_resolver_handle, wallet_id, |mnemonic| {
141+
master_key_for_mnemonic(mnemonic, network)
146142
})
147143
}
148144

@@ -179,6 +175,19 @@ pub(crate) unsafe fn resolve_seed_from_resolver_classified(
179175
mnemonic_resolver_handle: *mut rs_sdk_ffi::MnemonicResolverHandle,
180176
wallet_id: &[u8; 32],
181177
) -> Result<Zeroizing<[u8; 64]>, ResolveFailure> {
178+
with_resolved_mnemonic(mnemonic_resolver_handle, wallet_id, seed_for_mnemonic)
179+
}
180+
181+
/// Reads the mnemonic of `wallet_id` through the resolver into a zeroizing
182+
/// buffer and returns what `convert` makes of it.
183+
///
184+
/// # Safety
185+
/// Same contract as [`resolve_seed_from_resolver`].
186+
unsafe fn with_resolved_mnemonic<T>(
187+
mnemonic_resolver_handle: *mut rs_sdk_ffi::MnemonicResolverHandle,
188+
wallet_id: &[u8; 32],
189+
convert: impl FnOnce(&str) -> Result<T, PlatformWalletError>,
190+
) -> Result<T, ResolveFailure> {
182191
use rs_sdk_ffi::{mnemonic_resolver_result, MNEMONIC_RESOLVER_BUFFER_CAPACITY};
183192
use std::ffi::c_void;
184193

@@ -236,16 +245,13 @@ pub(crate) unsafe fn resolve_seed_from_resolver_classified(
236245
format!("mnemonic resolver: returned invalid UTF-8: {e}"),
237246
))
238247
})?;
239-
let mnemonic = parse_mnemonic_any_language(mnemonic_str).map_err(|e| {
248+
// What is stored cannot yield a key, however often it is read.
249+
convert(mnemonic_str).map_err(|e| {
240250
ResolveFailure::permanent(PlatformWalletFFIResult::err(
241251
PlatformWalletFFIResultCode::ErrorWalletOperation,
242252
format!("mnemonic resolver: returned an invalid mnemonic: {e}"),
243253
))
244-
})?;
245-
246-
let seed: Zeroizing<[u8; 64]> = Zeroizing::new(mnemonic.to_seed(""));
247-
drop(mnemonic);
248-
Ok(seed)
254+
})
249255
}
250256

251257
/// Build the DIP-9 identity-authentication derivation path

‎packages/rs-platform-wallet/src/lib.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ pub use wallet::masternode_withdrawal::{
9191
MasternodeWithdrawalKey, MasternodeWithdrawalKeys, MasternodeWithdrawalRequest,
9292
};
9393
pub use wallet::mnemonic::{
94-
first_receive_address_for_mnemonic, parse_mnemonic_any_language, wallet_id_for_mnemonic,
94+
first_receive_address_for_mnemonic, master_key_for_mnemonic, parse_mnemonic_any_language,
95+
seed_for_mnemonic, wallet_id_for_mnemonic,
9596
};
9697
pub use wallet::platform_wallet::PlatformWalletInfo;
9798
#[cfg(feature = "shielded")]

‎packages/rs-platform-wallet/src/wallet/mnemonic.rs‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub fn wallet_id_for_mnemonic(
2424
mnemonic: &str,
2525
network: Network,
2626
) -> Result<WalletId, PlatformWalletError> {
27-
let seed = seed_of(mnemonic)?;
27+
let seed = seed_for_mnemonic(mnemonic)?;
2828
let root = RootExtendedPrivKey::new_master(&*seed).map_err(key_derivation)?;
2929
Ok(Wallet::compute_wallet_id_from_root_extended_pub_key(
3030
&root.to_root_extended_pub_key(),
@@ -45,8 +45,7 @@ pub fn first_receive_address_for_mnemonic(
4545
let path = account_type
4646
.derivation_path(network)
4747
.map_err(key_derivation)?;
48-
let seed = seed_of(mnemonic)?;
49-
let mut master = ExtendedPrivKey::new_master(network, &*seed).map_err(key_derivation)?;
48+
let mut master = master_key_for_mnemonic(mnemonic, network)?;
5049
let account_xpriv = master.derive_priv(&path);
5150
master.private_key.zeroize();
5251
let mut account_xpriv = account_xpriv.map_err(key_derivation)?;
@@ -70,12 +69,25 @@ pub fn parse_mnemonic_any_language(phrase: &str) -> Result<Mnemonic, &'static st
7069
Mnemonic::from_phrase(phrase).map_err(|_| "phrase does not match any supported BIP-39 wordlist")
7170
}
7271

73-
fn seed_of(mnemonic: &str) -> Result<Zeroizing<[u8; 64]>, PlatformWalletError> {
72+
/// Returns the 64-byte BIP-39 seed of `mnemonic`, wiped when dropped.
73+
pub fn seed_for_mnemonic(mnemonic: &str) -> Result<Zeroizing<[u8; 64]>, PlatformWalletError> {
7474
let mnemonic = parse_mnemonic_any_language(mnemonic)
7575
.map_err(|e| PlatformWalletError::InvalidParameter(e.to_string()))?;
7676
Ok(Zeroizing::new(mnemonic.to_seed("")))
7777
}
7878

79+
/// Returns the BIP-32 master key of `mnemonic` on `network`.
80+
///
81+
/// `ExtendedPrivKey` wipes nothing on drop, so the caller zeroizes its
82+
/// `private_key` once done.
83+
pub fn master_key_for_mnemonic(
84+
mnemonic: &str,
85+
network: Network,
86+
) -> Result<ExtendedPrivKey, PlatformWalletError> {
87+
let seed = seed_for_mnemonic(mnemonic)?;
88+
ExtendedPrivKey::new_master(network, &*seed).map_err(key_derivation)
89+
}
90+
7991
#[cfg(test)]
8092
mod tests {
8193
use super::*;
@@ -88,6 +100,10 @@ mod tests {
88100
#[test]
89101
fn should_match_the_id_of_a_created_wallet() {
90102
for phrase in [ENGLISH, FRENCH] {
103+
assert_eq!(
104+
seed_for_mnemonic(phrase).unwrap()[..],
105+
Mnemonic::from_phrase(phrase).unwrap().to_seed("")[..]
106+
);
91107
for network in [Network::Mainnet, Network::Testnet, Network::Devnet] {
92108
let wallet = Wallet::from_mnemonic(
93109
Mnemonic::from_phrase(phrase).unwrap(),

0 commit comments

Comments
 (0)