- Add SYNOR_BOOTSTRAP_PEERS env var for runtime seed node configuration - Implement secrets provider abstraction for faucet wallet key security (supports file-based secrets in /run/secrets for production) - Create WASM crypto crate foundation for web wallet (Ed25519, BIP-39) - Add DEPLOYMENT.md guide for testnet deployment - Add SECURITY_AUDIT_SCOPE.md for external security audit preparation - Document seed node deployment process in synor-network Security improvements: - Faucet now auto-detects /run/secrets for secure key storage - CORS already defaults to specific origins (https://faucet.synor.cc) - Bootstrap peers now configurable at runtime without recompilation
115 lines
3.4 KiB
Rust
115 lines
3.4 KiB
Rust
//! BIP-39 mnemonic support for WASM.
|
|
|
|
use bip39::{Language, Mnemonic as Bip39Mnemonic, MnemonicType};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
/// BIP-39 mnemonic phrase wrapper.
|
|
#[wasm_bindgen]
|
|
pub struct Mnemonic {
|
|
inner: Bip39Mnemonic,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl Mnemonic {
|
|
/// Generate a new random mnemonic with the specified word count.
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(word_count: u8) -> Result<Mnemonic, JsValue> {
|
|
let mnemonic_type = match word_count {
|
|
12 => MnemonicType::Words12,
|
|
15 => MnemonicType::Words15,
|
|
18 => MnemonicType::Words18,
|
|
21 => MnemonicType::Words21,
|
|
24 => MnemonicType::Words24,
|
|
_ => {
|
|
return Err(JsValue::from_str(
|
|
"Invalid word count. Must be 12, 15, 18, 21, or 24",
|
|
))
|
|
}
|
|
};
|
|
|
|
let mnemonic = Bip39Mnemonic::new(mnemonic_type, Language::English);
|
|
Ok(Mnemonic { inner: mnemonic })
|
|
}
|
|
|
|
/// Generate a 24-word mnemonic.
|
|
#[wasm_bindgen]
|
|
pub fn generate(word_count: u8) -> Result<Mnemonic, JsValue> {
|
|
Mnemonic::new(word_count)
|
|
}
|
|
|
|
/// Parse a mnemonic from a phrase.
|
|
#[wasm_bindgen(js_name = fromPhrase)]
|
|
pub fn from_phrase(phrase: &str) -> Result<Mnemonic, JsValue> {
|
|
let mnemonic = Bip39Mnemonic::from_phrase(phrase, Language::English)
|
|
.map_err(|e| JsValue::from_str(&format!("Invalid mnemonic: {:?}", e)))?;
|
|
Ok(Mnemonic { inner: mnemonic })
|
|
}
|
|
|
|
/// Get the mnemonic phrase as a string.
|
|
#[wasm_bindgen]
|
|
pub fn phrase(&self) -> String {
|
|
self.inner.phrase().to_string()
|
|
}
|
|
|
|
/// Get the mnemonic words as an array.
|
|
#[wasm_bindgen]
|
|
pub fn words(&self) -> Vec<String> {
|
|
self.inner
|
|
.phrase()
|
|
.split_whitespace()
|
|
.map(String::from)
|
|
.collect()
|
|
}
|
|
|
|
/// Get the word count.
|
|
#[wasm_bindgen(js_name = wordCount)]
|
|
pub fn word_count(&self) -> usize {
|
|
self.inner.phrase().split_whitespace().count()
|
|
}
|
|
|
|
/// Derive a 64-byte seed from the mnemonic.
|
|
#[wasm_bindgen(js_name = toSeed)]
|
|
pub fn to_seed(&self, passphrase: &str) -> Vec<u8> {
|
|
let seed = bip39::Seed::new(&self.inner, passphrase);
|
|
seed.as_bytes().to_vec()
|
|
}
|
|
|
|
/// Get the entropy bytes.
|
|
#[wasm_bindgen]
|
|
pub fn entropy(&self) -> Vec<u8> {
|
|
self.inner.entropy().to_vec()
|
|
}
|
|
|
|
/// Validate a mnemonic phrase.
|
|
#[wasm_bindgen]
|
|
pub fn validate(phrase: &str) -> bool {
|
|
Bip39Mnemonic::validate(phrase, Language::English).is_ok()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use wasm_bindgen_test::*;
|
|
|
|
#[wasm_bindgen_test]
|
|
fn test_mnemonic_generation() {
|
|
let mnemonic = Mnemonic::new(24).unwrap();
|
|
assert_eq!(mnemonic.word_count(), 24);
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn test_mnemonic_validation() {
|
|
assert!(Mnemonic::validate("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"));
|
|
assert!(!Mnemonic::validate("invalid mnemonic phrase"));
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn test_mnemonic_to_seed() {
|
|
let mnemonic = Mnemonic::from_phrase(
|
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
).unwrap();
|
|
let seed = mnemonic.to_seed("");
|
|
assert_eq!(seed.len(), 64);
|
|
}
|
|
}
|