Skip to main content

qp_rusty_crystals_hdwallet/
lib.rs

1//! # Quantus Network HD Wallet
2//!
3//! This crate provides hierarchical deterministic (HD) wallet functionality for post-quantum
4//! ML-DSA (Dilithium) keys, compatible
5#![cfg_attr(not(feature = "std"), no_std)]
6extern crate alloc;
7
8use crate::hderive::ExtendedPrivKey;
9use alloc::{
10	string::{String, ToString},
11	vec::Vec,
12};
13use bip39::{Language, Mnemonic};
14use core::str::FromStr;
15use qp_rusty_crystals_dilithium::ml_dsa_87::Keypair;
16
17use zeroize::Zeroize;
18
19#[cfg(test)]
20mod test_vectors;
21#[cfg(test)]
22mod tests;
23
24pub mod hderive;
25pub mod wormhole;
26
27pub use wormhole::WormholePair;
28
29// Import and re-export SensitiveBytes types from dilithium
30pub use qp_rusty_crystals_dilithium::{SensitiveBytes32, SensitiveBytes64};
31
32#[derive(Debug, thiserror::Error, PartialEq, Eq)]
33pub enum HDLatticeError {
34	#[error("BIP39 error: {0}")]
35	Bip39Error(String),
36	#[error("Key derivation failed: {0}")]
37	KeyDerivationFailed(String),
38	#[error("Bad entropy bit count: {0}")]
39	BadEntropyBitCount(usize),
40	#[error("Mnemonic derivation failed: {0}")]
41	MnemonicDerivationFailed(String),
42	#[error("Invalid wormhole path: {0}")]
43	InvalidWormholePath(String),
44	#[error("Invalid BIP44 path: {0}")]
45	InvalidPath(String),
46	#[error("hderive error: {0:?}")]
47	GenericError(hderive::Error),
48}
49
50pub const ROOT_PATH: &str = "m";
51pub const PURPOSE: &str = "44'";
52pub const QUANTUS_DILITHIUM_CHAIN_ID: &str = "189189'";
53pub const QUANTUS_WORMHOLE_CHAIN_ID: &str = "189189189'";
54
55/// Convert a BIP39 mnemonic phrase to a seed
56///
57/// This function takes ownership of the mnemonic string for security.
58/// Users must explicitly choose to move or copy their mnemonic:
59///
60/// ```rust
61/// use qp_rusty_crystals_hdwallet::mnemonic_to_seed;
62/// let mnemonic = "word word word...".to_string();
63///
64/// // Move the mnemonic (recommended for single use)
65/// let seed = mnemonic_to_seed(mnemonic, None);
66/// // mnemonic is now consumed and zeroized
67///
68/// // Or explicitly copy for multiple uses
69/// let mnemonic = "word word word...".to_string();
70/// let seed1 = mnemonic_to_seed(mnemonic.clone(), None);
71/// let seed2 = mnemonic_to_seed(mnemonic, None); // consumes original
72/// ```
73///
74/// # Security Note
75/// This function performs expensive PBKDF2 key stretching (2048 iterations).
76/// The mnemonic string is zeroized before returning.
77/// The returned seed contains sensitive cryptographic material and should be
78/// zeroized when no longer needed.
79pub fn mnemonic_to_seed(
80	mut mnemonic: String,
81	passphrase: Option<&str>,
82) -> Result<[u8; 64], HDLatticeError> {
83	// Parse the mnemonic
84	let parsed_mnemonic = Mnemonic::parse_in_normalized(Language::English, &mnemonic)
85		.map_err(|e| HDLatticeError::Bip39Error(e.to_string()))?;
86
87	// Generate seed from mnemonic (expensive PBKDF2 operation)
88	let seed: [u8; 64] = parsed_mnemonic.to_seed_normalized(passphrase.unwrap_or(""));
89
90	// Zeroize the mnemonic string
91	mnemonic.zeroize();
92
93	Ok(seed)
94}
95
96/// Derive a Dilithium keypair from a seed at the given BIP44 path
97///
98/// # Security Note
99/// This function takes ownership of the seed for security (move semantics).
100/// The seed parameter is zeroized before returning.
101pub fn derive_key_from_seed(seed: SensitiveBytes64, path: &str) -> Result<Keypair, HDLatticeError> {
102	// Validate the derivation path
103	check_derivation_path(path)?;
104
105	// Derive entropy at the specified path
106	let xpriv = ExtendedPrivKey::derive(seed.as_bytes(), path)
107		.map_err(|_e| HDLatticeError::KeyDerivationFailed(path.to_string()))?;
108	let mut secret = xpriv.secret();
109	let derived_entropy = SensitiveBytes32::from(&mut secret);
110
111	// Generate keypair from derived entropy
112	let keypair = Keypair::generate(derived_entropy);
113
114	// seed and derived_entropy are automatically zeroized when they drop
115
116	Ok(keypair)
117}
118
119/// Keypair derivation from mnemonic with passphrase
120pub fn derive_key_from_mnemonic(
121	mnemonic: &str,
122	passphrase: Option<&str>,
123	path: &str,
124) -> Result<Keypair, HDLatticeError> {
125	let mut seed = mnemonic_to_seed(mnemonic.to_string(), passphrase)?;
126	derive_key_from_seed(SensitiveBytes64::from(&mut seed), path)
127}
128
129/// Wormhole pair derivation from mnemonic with passphrase
130pub fn derive_wormhole_from_mnemonic(
131	mnemonic: &str,
132	passphrase: Option<&str>,
133	path: &str,
134) -> Result<WormholePair, HDLatticeError> {
135	let mut seed = mnemonic_to_seed(mnemonic.to_string(), passphrase)?;
136	generate_wormhole_from_seed(SensitiveBytes64::from(&mut seed), path)
137}
138
139/// Generate a wormhole pair from a seed at the given path
140///
141/// # Security Note
142/// This function takes ownership of the seed for security (move semantics).
143/// The seed parameter is zeroized before returning.
144pub fn generate_wormhole_from_seed(
145	seed: SensitiveBytes64,
146	path: &str,
147) -> Result<WormholePair, HDLatticeError> {
148	// Validate wormhole path
149	if path.split("/").nth(2) != Some(QUANTUS_WORMHOLE_CHAIN_ID) {
150		return Err(HDLatticeError::InvalidWormholePath(path.to_string()));
151	}
152
153	// Validate the derivation path
154	check_derivation_path(path)?;
155
156	// Derive entropy at the specified path
157	let xpriv = ExtendedPrivKey::derive(seed.as_bytes(), path)
158		.map_err(|_e| HDLatticeError::KeyDerivationFailed(path.to_string()))?;
159	let mut secret = xpriv.secret();
160	let derived_entropy = SensitiveBytes32::from(&mut secret);
161
162	// Generate wormhole pair
163	let wormhole_pair = WormholePair::generate_new(derived_entropy);
164
165	// seed and derived_entropy are automatically zeroized when they drop
166
167	Ok(wormhole_pair)
168}
169
170/// Validate a derivation path — parsing itself enforces hardened-only.
171fn check_derivation_path(path: &str) -> Result<(), HDLatticeError> {
172	crate::hderive::DerivationPath::from_str(path).map_err(HDLatticeError::GenericError)?;
173	Ok(())
174}
175
176/// Generate a new random mnemonic with 24 words = 32 bytes
177///
178/// This function takes ownership of the entropy for security (move semantics).
179/// The entropy parameter is zeroized before returning.
180///
181/// # Security Note
182/// Always use cryptographically secure random entropy (e.g., from `getrandom::getrandom()`).
183/// Never use predictable strings, timestamps, or user input as entropy sources.
184pub fn generate_mnemonic(entropy: SensitiveBytes32) -> Result<String, HDLatticeError> {
185	// Create mnemonic from entropy
186	let mnemonic = Mnemonic::from_entropy(entropy.as_bytes())
187		.map_err(|e| HDLatticeError::MnemonicDerivationFailed(e.to_string()))?;
188
189	let result = mnemonic.words().collect::<Vec<&str>>().join(" ");
190
191	// entropy is automatically zeroized when it drops
192
193	Ok(result)
194}