Skip to content

Commit

Permalink
nostr: allow to specify the whole 5 level path for NIP06
Browse files Browse the repository at this point in the history
* ffi(nostr): add `Keys::from_mnemonic` and `Keys::from_mnemonic_advanced`
* js(nostr): update `JsKeys::from_mnemonic`

Closes #319
  • Loading branch information
yukibtc committed Mar 13, 2024
1 parent 01c7ab7 commit 9d377ff
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 27 deletions.
6 changes: 6 additions & 0 deletions bindings/nostr-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ impl From<nostr::nips::nip05::Error> for NostrError {
}
}

impl From<nostr::nips::nip06::Error> for NostrError {
fn from(e: nostr::nips::nip06::Error) -> NostrError {
Self::Generic(e.to_string())
}
}

impl From<nostr::nips::nip11::Error> for NostrError {
fn from(e: nostr::nips::nip11::Error) -> NostrError {
Self::Generic(e.to_string())
Expand Down
33 changes: 28 additions & 5 deletions bindings/nostr-ffi/src/key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod secret_key;

pub use self::public_key::PublicKey;
pub use self::secret_key::SecretKey;
use crate::error::{NostrError, Result};
use crate::error::Result;

#[derive(Object)]
pub struct Keys {
Expand Down Expand Up @@ -76,18 +76,41 @@ impl Keys {

/// Derive `Keys` from BIP-39 mnemonics (ENGLISH wordlist).
///
/// By default no passphrase is used and account is set to `0`.
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[uniffi::constructor]
pub fn from_mnemonic(mnemonic: String, passphrase: Option<String>) -> Result<Self> {
Ok(Self {
inner: key::Keys::from_mnemonic(mnemonic, passphrase)?,
})
}

/// Derive `Keys` from BIP-39 mnemonics with **custom account** (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[uniffi::constructor]
pub fn from_mnemonic_with_account(
mnemonic: String,
passphrase: Option<String>,
account: Option<u32>,
) -> Result<Self> {
Ok(Self {
inner: key::Keys::from_mnemonic_with_account(mnemonic, passphrase, account)?,
})
}

/// Derive `Keys` from BIP-39 mnemonics with **custom** `account`, `type` and/or `index` (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[uniffi::constructor]
pub fn from_mnemonic(
pub fn from_mnemonic_advanced(
mnemonic: String,
passphrase: Option<String>,
account: Option<u32>,
typ: Option<u32>,
index: Option<u32>,
) -> Result<Self> {
Ok(Self {
inner: key::Keys::from_mnemonic_with_account(mnemonic, passphrase, account)
.map_err(|e| NostrError::Generic(e.to_string()))?,
inner: key::Keys::from_mnemonic_advanced(mnemonic, passphrase, account, typ, index)?,
})
}

Expand Down
19 changes: 17 additions & 2 deletions bindings/nostr-js/src/key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,25 @@ impl JsKeys {
}

/// Derive keys from BIP-39 mnemonics (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[wasm_bindgen(js_name = fromMnemonic)]
pub fn from_mnemonic(mnemonic: &str, passphrase: Option<String>) -> Result<JsKeys> {
pub fn from_mnemonic(
mnemonic: &str,
passphrase: Option<String>,
account: Option<u32>,
typ: Option<u32>,
index: Option<u32>,
) -> Result<JsKeys> {
Ok(Self {
inner: Keys::from_mnemonic(mnemonic, passphrase.as_deref()).map_err(into_err)?,
inner: Keys::from_mnemonic_advanced(
mnemonic,
passphrase.as_deref(),
account,
typ,
index,
)
.map_err(into_err)?,
})
}

Expand Down
2 changes: 1 addition & 1 deletion crates/nostr/examples/embedded/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn main() -> ! {

// Restore from menmonic
let mnemonic: &str = "equal dragon fabric refuse stable cherry smoke allow alley easy never medal attend together lumber movie what sad siege weather matrix buffalo state shoot";
let keys = Keys::from_mnemonic_with_ctx(&secp, mnemonic, None, None).unwrap();
let keys = Keys::from_mnemonic_with_ctx(&secp, mnemonic, None, None, None, None).unwrap();
hprintln!("\nRestore keys from mnemonic:").unwrap();
print_keys(&keys);

Expand Down
98 changes: 79 additions & 19 deletions crates/nostr/src/nips/nip06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
//!
//! <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[cfg(feature = "std")]
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;

use bip39::Mnemonic;
use bitcoin::bip32::{DerivationPath, ExtendedPrivKey};
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
use bitcoin::hashes::{sha512, Hash, HashEngine};
#[cfg(feature = "std")]
Expand All @@ -25,6 +25,9 @@ use bitcoin::Network;
use crate::SECP256K1;
use crate::{Keys, SecretKey};

const PURPOSE: u32 = 44;
const COIN: u32 = 1237;

/// `NIP06` error
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
Expand Down Expand Up @@ -58,42 +61,72 @@ impl From<bip39::Error> for Error {
}
}

#[allow(missing_docs)]
/// NIP06 utils
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
pub trait FromMnemonic: Sized {
/// Error
type Err;

/// Derive from BIP-39 mnemonics (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[cfg(feature = "std")]
fn from_mnemonic<S>(mnemonic: S, passphrase: Option<S>) -> Result<Self, Self::Err>
where
S: Into<String>,
S: AsRef<str>,
{
Self::from_mnemonic_with_account(mnemonic, passphrase, None)
}

/// Derive from BIP-39 mnemonics with **custom account** (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[cfg(feature = "std")]
fn from_mnemonic_with_account<S>(
mnemonic: S,
passphrase: Option<S>,
account: Option<u32>,
) -> Result<Self, Self::Err>
where
S: Into<String>,
S: AsRef<str>,
{
Self::from_mnemonic_advanced(mnemonic, passphrase, account, None, None)
}

/// Derive from BIP-39 mnemonics with **custom** `account`, `type` and/or `index` (ENGLISH wordlist).
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
#[cfg(feature = "std")]
fn from_mnemonic_advanced<S>(
mnemonic: S,
passphrase: Option<S>,
account: Option<u32>,
r#type: Option<u32>,
index: Option<u32>,
) -> Result<Self, Self::Err>
where
S: AsRef<str>,
{
let passphrase: Option<String> = passphrase.map(|p| p.into());
Self::from_mnemonic_with_ctx(&SECP256K1, &mnemonic.into(), passphrase.as_deref(), account)
Self::from_mnemonic_with_ctx(&SECP256K1, mnemonic, passphrase, account, r#type, index)
}

/// Derive from BIP-39 mnemonics with **custom account** (ENGLISH wordlist).
fn from_mnemonic_with_ctx<C>(
///
/// By default `account`, `type` and `index` are set to `0`.
///
/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
fn from_mnemonic_with_ctx<C, S>(
secp: &Secp256k1<C>,
mnemonic: &str,
passphrase: Option<&str>,
mnemonic: S,
passphrase: Option<S>,
account: Option<u32>,
r#type: Option<u32>,
index: Option<u32>,
) -> Result<Self, Self::Err>
where
C: Signing;
C: Signing,
S: AsRef<str>;
}

#[deprecated(since = "0.29.0")]
Expand All @@ -112,22 +145,48 @@ pub trait GenerateMnemonic {
impl FromMnemonic for Keys {
type Err = Error;

fn from_mnemonic_with_ctx<C>(
fn from_mnemonic_with_ctx<C, S>(
secp: &Secp256k1<C>,
mnemonic: &str,
passphrase: Option<&str>,
mnemonic: S,
passphrase: Option<S>,
account: Option<u32>,
r#type: Option<u32>,
index: Option<u32>,
) -> Result<Self, Self::Err>
where
C: Signing,
S: AsRef<str>,
{
let mnemonic: Mnemonic = Mnemonic::from_str(mnemonic)?;
let seed: [u8; 64] = mnemonic.to_seed_normalized(passphrase.unwrap_or_default());
// Parse menmonic
let mnemonic: Mnemonic = Mnemonic::from_str(mnemonic.as_ref())?;

// Convert mnemonic to seed
let seed: [u8; 64] = mnemonic
.to_seed_normalized(passphrase.as_ref().map(|s| s.as_ref()).unwrap_or_default());

// Derive BIP32 root key
let root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?;

// Unwrap idx
let account: u32 = account.unwrap_or_default();
let path = DerivationPath::from_str(&format!("m/44'/1237'/{account}'/0/0"))?;
let _type: u32 = r#type.unwrap_or_default();
let index: u32 = index.unwrap_or_default();

// Compose derivation path
let path: Vec<ChildNumber> = vec![
ChildNumber::from_hardened_idx(PURPOSE)?,
ChildNumber::from_hardened_idx(COIN)?,
ChildNumber::from_hardened_idx(account)?,
ChildNumber::from_normal_idx(_type)?,
ChildNumber::from_normal_idx(index)?,
];
let path = DerivationPath::from(path);

// Derive secret key
let child_xprv = root_key.derive_priv(secp, &path)?;
let secret_key = SecretKey::from(child_xprv.private_key);

// Compose keys
Ok(Self::new_with_ctx(secp, secret_key))
}
}
Expand Down Expand Up @@ -172,7 +231,8 @@ mod tests {
];

for (mnemonic, expected_secret_key) in list.into_iter() {
let keys = Keys::from_mnemonic_with_ctx(&secp, mnemonic, None, None).unwrap();
let keys =
Keys::from_mnemonic_with_ctx(&secp, mnemonic, None, None, None, None).unwrap();
assert_eq!(
keys.secret_key().unwrap(),
&SecretKey::from_str(expected_secret_key).unwrap()
Expand Down

0 comments on commit 9d377ff

Please sign in to comment.