guarded memory for bip39 seeds and bip32 keys. mlocked, wiped on drop, never copied into a gc heap.
seedlock = "0.1"a javascript string is immutable, garbage collected, and copied all over the heap. you cannot zero a mnemonic in javascript. every js wallet has this hole and none of them can close it from js.
seedlock keeps the phrase, the seed and every derived key in memory that:
- is
mlocked so it never reaches swap - is zeroed on drop
- compares in constant time
- prints as
redactedinDebug - is wiped in the caller's buffer on import
use seedlock::{Mnemonic, WordCount, XPrv};
// generate. the phrase never exists outside guarded memory
let m = Mnemonic::generate(WordCount::W12)?;
// or import. your bytes are zeroed for you
let mut phrase = read_from_user();
let m = Mnemonic::from_bytes(&mut phrase)?;
assert!(phrase.iter().all(|&b| b == 0));
let seed = m.to_seed("");
let master = XPrv::from_seed(&seed)?;
let account = master.derive_path("m/44'/60'/0'/0/0")?;
let pubkey = account.public_key(); // safe to hand out
let sig = account.sign_prehash(&digest)?; // key used in place, never returnedGuarded |
mlocked, zeroizing byte buffer. constant time PartialEq |
Mnemonic::generate(WordCount) |
fresh entropy from the os |
Mnemonic::from_bytes(&mut [u8]) |
validates the checksum, wipes your buffer |
Mnemonic::to_seed(passphrase) |
pbkdf2-hmac-sha512, 2048 rounds |
XPrv::from_seed(&Guarded) |
bip32 master key |
XPrv::derive_path("m/44'/60'/0'/0/0") |
' or h for hardened |
XPrv::public_key() |
compressed secp256k1, 33 bytes |
XPrv::sign_prehash(&[u8; 32]) |
ecdsa, 64 bytes |
12, 15, 18, 21 and 24 word phrases.
bip39 english vectors (empty and TREZOR passphrases) and bip32 test vector 1. 23 tests.
mlock is unix only. on other platforms the buffer still zeroes on drop, it just is not pinned. Guarded::is_locked() tells you which you got.
expose_secret() exists but is the escape hatch, not the api. reach for sign_prehash first.
MIT