lssa/lee/key_protocol/src/key_management/secret_holders.rs

288 lines
9.2 KiB
Rust
Raw Normal View History

2025-09-04 17:49:55 +03:00
use bip39::Mnemonic;
2025-10-17 16:04:09 -03:00
use common::HashType;
2026-05-12 18:17:56 -04:00
use ml_kem;
use lee_core::{
2025-09-17 08:59:14 +03:00
NullifierPublicKey, NullifierSecretKey,
2026-05-12 18:17:56 -04:00
encryption::ViewingPublicKey,
2025-09-17 08:59:14 +03:00
};
2026-03-04 18:42:33 +03:00
use rand::{RngCore as _, rngs::OsRng};
use serde::{Deserialize, Serialize};
2026-03-04 18:42:33 +03:00
use sha2::{Digest as _, digest::FixedOutput as _};
2024-10-30 12:32:36 +02:00
2025-11-26 00:27:20 +03:00
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
2024-10-30 12:32:36 +02:00
/// Produces `TopSecretKeyHolder` objects.
2026-03-14 03:20:37 +03:00
#[derive(Debug)]
2024-10-30 12:32:36 +02:00
pub struct SeedHolder {
2025-11-26 00:27:20 +03:00
// ToDo: Needs to be vec as serde derives is not implemented for [u8; 64]
2025-09-05 14:47:58 +03:00
pub(crate) seed: Vec<u8>,
2024-10-30 12:32:36 +02:00
}
2025-11-26 00:27:20 +03:00
/// Secret spending key object. Can produce `PrivateKeyHolder` objects.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2026-03-19 18:01:15 +02:00
pub struct SecretSpendingKey(pub [u8; 32]);
2026-05-12 18:17:56 -04:00
/// Viewing secret key: the KEM seed split into its two 32-byte halves `d` and `r` (= z in
/// FIPS 203), from which the ML-KEM 768 decapsulation key is derived deterministically.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ViewingSecretKey {
pub d: [u8; 32],
pub r: [u8; 32],
}
2024-10-30 12:32:36 +02:00
2026-03-03 23:21:08 +03:00
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
/// for recepient.
2026-05-12 18:17:56 -04:00
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
2025-09-05 14:47:58 +03:00
pub struct PrivateKeyHolder {
pub nullifier_secret_key: NullifierSecretKey,
2026-03-19 18:01:15 +02:00
pub viewing_secret_key: ViewingSecretKey,
2024-10-30 12:32:36 +02:00
}
impl SeedHolder {
2026-03-03 23:21:08 +03:00
#[must_use]
2024-10-30 12:32:36 +02:00
pub fn new_os_random() -> Self {
2025-09-04 17:49:55 +03:00
let mut enthopy_bytes: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut enthopy_bytes);
2024-10-30 12:32:36 +02:00
2025-11-26 14:53:26 +02:00
let mnemonic = Mnemonic::from_entropy(&enthopy_bytes)
.expect("Enthropy must be a multiple of 32 bytes");
2025-09-05 14:47:58 +03:00
let seed_wide = mnemonic.to_seed("mnemonic");
2024-10-30 12:32:36 +02:00
Self {
2025-09-05 14:47:58 +03:00
seed: seed_wide.to_vec(),
2024-10-30 12:32:36 +02:00
}
}
2026-03-03 23:21:08 +03:00
#[must_use]
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>
2026-01-20 16:47:34 +01:00
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,
)
}
#[must_use]
pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self {
2025-11-04 16:09:04 +02:00
let seed_wide = mnemonic.to_seed(passphrase);
Self {
seed: seed_wide.to_vec(),
}
}
2026-03-03 23:21:08 +03:00
#[must_use]
2025-10-17 16:04:09 -03:00
pub fn generate_secret_spending_key_hash(&self) -> HashType {
let mut hash = hmac_sha512::HMAC::mac(&self.seed, "LEE_seed");
2024-10-30 12:32:36 +02:00
2025-09-05 14:47:58 +03:00
for _ in 1..2048 {
hash = hmac_sha512::HMAC::mac(hash, "LEE_seed");
2025-09-05 14:47:58 +03:00
}
2024-10-30 12:32:36 +02:00
2025-11-26 00:27:20 +03:00
// Safe unwrap
HashType(*hash.first_chunk::<32>().unwrap())
2024-10-30 12:32:36 +02:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
2025-09-15 14:04:49 +03:00
pub fn produce_top_secret_key_holder(&self) -> SecretSpendingKey {
SecretSpendingKey(self.generate_secret_spending_key_hash().into())
2024-10-30 12:32:36 +02:00
}
}
2025-09-15 14:04:49 +03:00
impl SecretSpendingKey {
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-18 18:44:07 -04:00
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
2026-01-27 16:00:42 -05:00
pub fn generate_nullifier_secret_key(&self, index: Option<u32>) -> NullifierSecretKey {
2026-03-03 23:21:08 +03:00
const PREFIX: &[u8; 8] = b"LEE/keys";
const SUFFIX_1: &[u8; 1] = &[1];
const SUFFIX_2: &[u8; 19] = &[0; 19];
2026-01-27 16:00:42 -05:00
let index = match index {
2026-03-04 18:42:33 +03:00
None => 0_u32,
2026-01-27 16:00:42 -05:00
_ => index.expect("Expect a valid u32"),
};
let mut hasher = sha2::Sha256::new();
hasher.update(PREFIX);
2025-09-15 14:04:49 +03:00
hasher.update(self.0);
2026-01-27 16:00:42 -05:00
hasher.update(SUFFIX_1);
2026-02-25 15:18:27 -05:00
hasher.update(index.to_be_bytes());
2026-01-27 16:00:42 -05:00
hasher.update(SUFFIX_2);
2025-09-05 14:47:58 +03:00
2025-09-15 14:04:49 +03:00
<NullifierSecretKey>::from(hasher.finalize_fixed())
2025-09-05 14:47:58 +03:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-18 18:44:07 -04:00
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
2026-05-13 20:43:12 -04:00
pub fn generate_viewing_secret_seed_key(&self, index: Option<u32>) -> ViewingSecretKey {
2026-03-03 23:21:08 +03:00
const PREFIX: &[u8; 8] = b"LEE/keys";
const SUFFIX_1: &[u8; 1] = &[2];
const SUFFIX_2: &[u8; 19] = &[0; 19];
2026-01-27 16:00:42 -05:00
let index = match index {
2026-03-04 18:42:33 +03:00
None => 0_u32,
2026-01-27 16:00:42 -05:00
_ => index.expect("Expect a valid u32"),
};
2025-09-05 14:47:58 +03:00
2026-05-12 18:17:56 -04:00
let mut bytes: Vec<u8> = Vec::with_capacity(64);
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(&self.0);
bytes.extend_from_slice(SUFFIX_1);
bytes.extend_from_slice(&index.to_be_bytes());
bytes.extend_from_slice(SUFFIX_2);
2026-05-13 20:43:12 -04:00
let bytes: [u8; 64] = bytes
.try_into()
.expect("`generate_viewing_secret_key`: bytes must be exactly 64");
2026-05-12 18:17:56 -04:00
2026-05-13 20:43:12 -04:00
let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed");
ViewingSecretKey {
d: *full_seed
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32"),
r: *full_seed
.last_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get last 32"),
}
2026-05-12 18:17:56 -04:00
}
2024-10-30 12:32:36 +02:00
2026-05-12 18:17:56 -04:00
#[must_use]
pub fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey {
ViewingSecretKey {
d: *seed.first_chunk::<32>().expect("seed is 64 bytes"),
r: *seed.last_chunk::<32>().expect("seed is 64 bytes"),
}
2024-10-30 12:32:36 +02:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-01-27 16:00:42 -05:00
pub fn produce_private_key_holder(&self, index: Option<u32>) -> PrivateKeyHolder {
2025-09-05 14:47:58 +03:00
PrivateKeyHolder {
2026-01-27 16:00:42 -05:00
nullifier_secret_key: self.generate_nullifier_secret_key(index),
2026-05-13 20:43:12 -04:00
viewing_secret_key: self.generate_viewing_secret_seed_key(index),
2024-10-30 12:32:36 +02:00
}
}
}
2026-05-12 18:17:56 -04:00
impl From<&ViewingSecretKey> for ViewingPublicKey {
fn from(sk: &ViewingSecretKey) -> Self {
use ml_kem::{Kem, KeyExport as _, MlKem768, Seed};
let mut seed_bytes = [0u8; 64];
seed_bytes[..32].copy_from_slice(&sk.d);
seed_bytes[32..].copy_from_slice(&sk.r);
let dk = <MlKem768 as Kem>::DecapsulationKey::from_seed(Seed::from(seed_bytes));
ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec())
}
}
2025-09-05 14:47:58 +03:00
impl PrivateKeyHolder {
2026-03-03 23:21:08 +03:00
#[must_use]
2025-09-15 14:04:49 +03:00
pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey {
2026-01-21 17:27:23 -05:00
(&self.nullifier_secret_key).into()
2025-09-05 14:47:58 +03:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-01-21 17:27:23 -05:00
pub fn generate_viewing_public_key(&self) -> ViewingPublicKey {
2026-05-12 18:17:56 -04:00
ViewingPublicKey::from(&self.viewing_secret_key)
2025-09-05 14:47:58 +03:00
}
}
#[cfg(test)]
mod tests {
use super::*;
2026-01-21 17:48:10 -05:00
// TODO? are these necessary?
2025-09-05 14:47:58 +03:00
#[test]
2026-01-21 17:27:23 -05:00
fn seed_generation_test() {
2025-09-05 14:47:58 +03:00
let seed_holder = SeedHolder::new_os_random();
assert_eq!(seed_holder.seed.len(), 64);
}
#[test]
2026-01-21 17:27:23 -05:00
fn ssk_generation_test() {
2025-09-05 14:47:58 +03:00
let seed_holder = SeedHolder::new_os_random();
assert_eq!(seed_holder.seed.len(), 64);
2026-03-04 18:42:33 +03:00
let _hash = seed_holder.generate_secret_spending_key_hash();
2025-09-05 14:47:58 +03:00
}
#[test]
2026-01-21 17:27:23 -05:00
fn ivs_generation_test() {
2025-09-05 14:47:58 +03:00
let seed_holder = SeedHolder::new_os_random();
assert_eq!(seed_holder.seed.len(), 64);
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
2026-05-13 20:43:12 -04:00
// Marvin-pq should drop seed from the fucntion name
let _vsk = top_secret_key_holder.generate_viewing_secret_seed_key(None);
2024-10-30 12:32:36 +02:00
}
2025-11-11 12:15:20 +02:00
#[test]
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>
2026-01-20 16:47:34 +01:00
fn two_seeds_recovered_same_from_same_mnemonic() {
let passphrase = "test_pass";
// Generate a mnemonic with random entropy
let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
// 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("");
2025-11-11 12:15:20 +02: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>
2026-01-20 16:47:34 +01:00
let seed1 = SeedHolder::from_mnemonic(&mnemonic, "");
let seed2 = SeedHolder::from_mnemonic(&mnemonic, "");
2025-11-11 12:15:20 +02: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>
2026-01-20 16:47:34 +01:00
// Same mnemonic and passphrase should always produce the same seed
assert_eq!(seed1.seed, seed2.seed);
2025-11-11 12:15:20 +02:00
}
2024-10-30 12:32:36 +02:00
}