pub use bip39::{Language, Mnemonic};
use thiserror::Error;
use zeroize::Zeroize;
use crate::key::{Keys, SecretKey};
const PURPOSE: u32 = 44;
const COIN_TYPE: u32 = 1237;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WordCount {
Twelve,
Fifteen,
Eighteen,
TwentyOne,
TwentyFour,
}
impl WordCount {
#[must_use]
pub const fn as_count(self) -> usize {
match self {
Self::Twelve => 12,
Self::Fifteen => 15,
Self::Eighteen => 18,
Self::TwentyOne => 21,
Self::TwentyFour => 24,
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Nip06Error {
#[error("invalid BIP-39 mnemonic: {0}")]
Mnemonic(#[from] bip39::Error),
#[error("BIP-32 derivation produced an invalid secp256k1 scalar")]
InvalidDerivedKey,
#[error(transparent)]
Rng(#[from] crate::util::rng::RngError),
}
pub fn generate_mnemonic(word_count: WordCount) -> Result<Mnemonic, Nip06Error> {
let entropy_bytes = word_count.as_count() * 4 / 3;
let mnemonic = match entropy_bytes {
16 => Mnemonic::from_entropy_in(Language::English, &fresh::<16>()?)?,
20 => Mnemonic::from_entropy_in(Language::English, &fresh::<20>()?)?,
24 => Mnemonic::from_entropy_in(Language::English, &fresh::<24>()?)?,
28 => Mnemonic::from_entropy_in(Language::English, &fresh::<28>()?)?,
32 => Mnemonic::from_entropy_in(Language::English, &fresh::<32>()?)?,
_ => unreachable!("WordCount only yields 16/20/24/28/32 entropy bytes"),
};
Ok(mnemonic)
}
fn fresh<const N: usize>() -> Result<[u8; N], crate::util::rng::RngError> {
crate::util::rng::random_bytes::<N>()
}
pub fn derive_keys(mnemonic: &str, passphrase: Option<&str>) -> Result<Keys, Nip06Error> {
derive_keys_advanced(mnemonic, passphrase, 0, 0, 0)
}
pub fn derive_keys_advanced(
mnemonic: &str,
passphrase: Option<&str>,
account: u32,
chain_type: u32,
index: u32,
) -> Result<Keys, Nip06Error> {
let parsed = Mnemonic::parse_normalized(mnemonic)?;
let mut seed = parsed.to_seed_normalized(passphrase.unwrap_or_default());
let secret_bytes = bip32::derive_nostr_path(&seed, account, chain_type, index)?;
seed.zeroize();
let secret =
SecretKey::from_byte_array(secret_bytes).map_err(|_| Nip06Error::InvalidDerivedKey)?;
Ok(Keys::from_secret_key(secret))
}
#[allow(
clippy::expect_used,
clippy::unwrap_in_result,
reason = "see module-level comment: every expect guards a statically proved length invariant"
)]
mod bip32 {
use hmac::digest::KeyInit;
use hmac::{Hmac, Mac};
use sha2::Sha512;
use zeroize::Zeroize;
use super::{COIN_TYPE, Nip06Error, PURPOSE};
const MASTER_KEY: &[u8] = b"Bitcoin seed";
const HARDENED_OFFSET: u32 = 0x8000_0000;
const HALF: usize = 32;
type HmacSha512 = Hmac<Sha512>;
pub(super) fn derive_nostr_path(
seed: &[u8; 64],
account: u32,
chain_type: u32,
index: u32,
) -> Result<[u8; 32], Nip06Error> {
let (mut k, mut c) = master_key(seed);
for &(idx, hardened) in &[
(PURPOSE, true),
(COIN_TYPE, true),
(account, true),
(chain_type, false),
(index, false),
] {
(k, c) = ckd_priv(&k, &c, idx, hardened)?;
}
c.zeroize();
Ok(k)
}
fn split_halves(bytes: [u8; 64]) -> ([u8; HALF], [u8; HALF]) {
let (left, rest) = bytes
.split_first_chunk::<HALF>()
.expect("32 <= 64, statically");
let right: [u8; HALF] = rest.try_into().expect("64 - 32 = 32, statically");
(*left, right)
}
fn master_key(seed: &[u8]) -> ([u8; HALF], [u8; HALF]) {
let mut mac = <HmacSha512 as KeyInit>::new_from_slice(MASTER_KEY)
.expect("HMAC-SHA512 accepts any key up to its 128-byte block size");
mac.update(seed);
let bytes: [u8; 64] = mac.finalize().into_bytes().into();
split_halves(bytes)
}
fn ckd_priv(
parent_secret: &[u8; HALF],
parent_chain: &[u8; HALF],
index: u32,
hardened: bool,
) -> Result<([u8; HALF], [u8; HALF]), Nip06Error> {
let parent_sk = secp256k1::SecretKey::from_byte_array(*parent_secret)
.map_err(|_| Nip06Error::InvalidDerivedKey)?;
let child_index = if hardened {
index | HARDENED_OFFSET
} else {
index
};
let mut mac = <HmacSha512 as KeyInit>::new_from_slice(parent_chain)
.expect("HMAC-SHA512 accepts any key up to its 128-byte block size");
if hardened {
mac.update(&[0x00]);
mac.update(parent_secret);
} else {
let parent_pk = secp256k1::PublicKey::from_secret_key_global(&parent_sk);
mac.update(&parent_pk.serialize());
}
mac.update(&child_index.to_be_bytes());
let bytes: [u8; 64] = mac.finalize().into_bytes().into();
let (mut left, chain) = split_halves(bytes);
let scalar =
secp256k1::Scalar::from_be_bytes(left).map_err(|_| Nip06Error::InvalidDerivedKey)?;
let child_sk = parent_sk
.add_tweak(&scalar)
.map_err(|_| Nip06Error::InvalidDerivedKey)?;
let child_bytes = child_sk.secret_bytes();
left.zeroize();
Ok((child_bytes, chain))
}
}
#[cfg(test)]
mod tests {
use super::*;
const SPEC_VECTORS: &[(&str, &str)] = &[
(
"leader monkey parrot ring guide accident before fence cannon height naive bean",
"7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a",
),
(
"what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade",
"c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add",
),
];
#[test]
fn spec_vectors_match() {
for (sentence, expected) in SPEC_VECTORS {
let keys = derive_keys(sentence, None).unwrap();
assert_eq!(
keys.secret_key().to_hex(),
*expected,
"mismatch on mnemonic: {sentence}",
);
}
}
#[test]
fn passphrase_changes_derived_key() {
let mnemonic = SPEC_VECTORS[0].0;
let plain = derive_keys(mnemonic, None).unwrap();
let with_pw = derive_keys(mnemonic, Some("nostr")).unwrap();
assert_ne!(plain.secret_key().to_hex(), with_pw.secret_key().to_hex());
}
#[test]
fn account_changes_derived_key() {
let mnemonic = SPEC_VECTORS[0].0;
let acct0 = derive_keys_advanced(mnemonic, None, 0, 0, 0).unwrap();
let acct1 = derive_keys_advanced(mnemonic, None, 1, 0, 0).unwrap();
assert_ne!(acct0.secret_key().to_hex(), acct1.secret_key().to_hex());
}
#[test]
fn index_changes_derived_key() {
let mnemonic = SPEC_VECTORS[0].0;
let i0 = derive_keys_advanced(mnemonic, None, 0, 0, 0).unwrap();
let i1 = derive_keys_advanced(mnemonic, None, 0, 0, 1).unwrap();
assert_ne!(i0.secret_key().to_hex(), i1.secret_key().to_hex());
}
#[test]
fn malformed_mnemonic_rejected() {
let err = derive_keys("not a real mnemonic just words here", None).unwrap_err();
assert!(matches!(err, Nip06Error::Mnemonic(_)));
}
#[test]
fn generate_mnemonic_round_trips_through_derive() {
for &count in &[
WordCount::Twelve,
WordCount::Fifteen,
WordCount::Eighteen,
WordCount::TwentyOne,
WordCount::TwentyFour,
] {
let mnemonic = generate_mnemonic(count).unwrap();
assert_eq!(mnemonic.word_count(), count.as_count());
let _ = derive_keys(&mnemonic.to_string(), None).unwrap();
}
}
#[test]
fn surrounding_whitespace_is_tolerated() {
let canonical = SPEC_VECTORS[0].0;
let padded = format!("\t {canonical} \n");
let a = derive_keys(canonical, None).unwrap();
let b = derive_keys(&padded, None).unwrap();
assert_eq!(a.secret_key().to_hex(), b.secret_key().to_hex());
}
#[test]
fn uppercase_mnemonic_is_rejected() {
let err = derive_keys(&SPEC_VECTORS[0].0.to_uppercase(), None).unwrap_err();
assert!(matches!(err, Nip06Error::Mnemonic(_)));
}
}