Skip to main content

nostr/nips/nip06/
mod.rs

1// Copyright (c) 2022-2023 Yuki Kishimoto
2// Copyright (c) 2023-2025 Rust Nostr Developers
3// Distributed under the MIT software license
4
5//! NIP06: Basic key derivation from mnemonic seed phrase
6//!
7//! <https://github.com/nostr-protocol/nips/blob/master/06.md>
8
9use alloc::vec;
10use alloc::vec::Vec;
11use core::fmt;
12
13use bip39::Mnemonic;
14use secp256k1::{Secp256k1, Signing};
15
16mod bip32;
17
18use self::bip32::{ChildNumber, Xpriv};
19#[cfg(feature = "std")]
20use crate::SECP256K1;
21use crate::{Keys, SecretKey};
22
23const PURPOSE: u32 = 44;
24const COIN: u32 = 1237;
25
26/// `NIP06` error
27#[derive(Debug, Eq, PartialEq)]
28pub enum Error {
29    /// BIP32 error
30    BIP32(bip32::Error),
31    /// BIP39 error
32    BIP39(bip39::Error),
33}
34
35#[cfg(feature = "std")]
36impl std::error::Error for Error {}
37
38impl fmt::Display for Error {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::BIP32(e) => e.fmt(f),
42            Self::BIP39(e) => e.fmt(f),
43        }
44    }
45}
46
47impl From<bip32::Error> for Error {
48    fn from(e: bip32::Error) -> Self {
49        Self::BIP32(e)
50    }
51}
52
53impl From<bip39::Error> for Error {
54    fn from(e: bip39::Error) -> Self {
55        Self::BIP39(e)
56    }
57}
58
59/// NIP06 utils
60///
61/// <https://github.com/nostr-protocol/nips/blob/master/06.md>
62pub trait FromMnemonic: Sized {
63    /// Error
64    type Err;
65
66    /// Derive from BIP-39 mnemonics (ENGLISH wordlist).
67    ///
68    /// <https://github.com/nostr-protocol/nips/blob/master/06.md>
69    #[inline]
70    #[cfg(feature = "std")]
71    fn from_mnemonic<S>(mnemonic: S, passphrase: Option<S>) -> Result<Self, Self::Err>
72    where
73        S: AsRef<str>,
74    {
75        Self::from_mnemonic_with_account(mnemonic, passphrase, None)
76    }
77
78    /// Derive from BIP-39 mnemonics with **custom account** (ENGLISH wordlist).
79    ///
80    /// <https://github.com/nostr-protocol/nips/blob/master/06.md>
81    #[inline]
82    #[cfg(feature = "std")]
83    fn from_mnemonic_with_account<S>(
84        mnemonic: S,
85        passphrase: Option<S>,
86        account: Option<u32>,
87    ) -> Result<Self, Self::Err>
88    where
89        S: AsRef<str>,
90    {
91        Self::from_mnemonic_advanced(mnemonic, passphrase, account, None, None)
92    }
93
94    /// Derive from BIP-39 mnemonics with **custom** `account`, `type` and/or `index` (ENGLISH wordlist).
95    ///
96    /// <https://github.com/nostr-protocol/nips/blob/master/06.md>
97    #[inline]
98    #[cfg(feature = "std")]
99    fn from_mnemonic_advanced<S>(
100        mnemonic: S,
101        passphrase: Option<S>,
102        account: Option<u32>,
103        r#type: Option<u32>,
104        index: Option<u32>,
105    ) -> Result<Self, Self::Err>
106    where
107        S: AsRef<str>,
108    {
109        Self::from_mnemonic_with_ctx(SECP256K1, mnemonic, passphrase, account, r#type, index)
110    }
111
112    /// Derive from BIP-39 mnemonics with **custom account** (ENGLISH wordlist).
113    ///
114    /// By default `account`, `type` and `index` are set to `0`.
115    ///
116    /// <https://github.com/nostr-protocol/nips/blob/master/06.md>
117    fn from_mnemonic_with_ctx<C, S>(
118        secp: &Secp256k1<C>,
119        mnemonic: S,
120        passphrase: Option<S>,
121        account: Option<u32>,
122        r#type: Option<u32>,
123        index: Option<u32>,
124    ) -> Result<Self, Self::Err>
125    where
126        C: Signing,
127        S: AsRef<str>;
128}
129
130impl FromMnemonic for Keys {
131    type Err = Error;
132
133    fn from_mnemonic_with_ctx<C, S>(
134        secp: &Secp256k1<C>,
135        mnemonic: S,
136        passphrase: Option<S>,
137        account: Option<u32>,
138        r#type: Option<u32>,
139        index: Option<u32>,
140    ) -> Result<Self, Self::Err>
141    where
142        C: Signing,
143        S: AsRef<str>,
144    {
145        // Parse mnemonic
146        let mnemonic: Mnemonic = Mnemonic::parse_normalized(mnemonic.as_ref())?;
147
148        // Convert mnemonic to seed
149        let seed: [u8; 64] = mnemonic
150            .to_seed_normalized(passphrase.as_ref().map(|s| s.as_ref()).unwrap_or_default());
151
152        // Derive BIP32 root key
153        let root_key: Xpriv = Xpriv::new_master(&seed)?;
154
155        // Unwrap idx
156        let account: u32 = account.unwrap_or_default();
157        let _type: u32 = r#type.unwrap_or_default();
158        let index: u32 = index.unwrap_or_default();
159
160        // Compose derivation path
161        let path: Vec<ChildNumber> = vec![
162            ChildNumber::from_hardened_idx(PURPOSE)?,
163            ChildNumber::from_hardened_idx(COIN)?,
164            ChildNumber::from_hardened_idx(account)?,
165            ChildNumber::from_normal_idx(_type)?,
166            ChildNumber::from_normal_idx(index)?,
167        ];
168
169        // Derive secret key
170        let child_xprv = root_key.derive_xpriv(secp, path);
171        let secret_key = SecretKey::from(child_xprv.private_key);
172
173        // Compose keys
174        Ok(Self::new_with_ctx(secp, secret_key))
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use core::str::FromStr;
181
182    use super::*;
183
184    #[test]
185    fn test_nip06() {
186        let secp = Secp256k1::new();
187
188        let list = vec![
189            ("equal dragon fabric refuse stable cherry smoke allow alley easy never medal attend together lumber movie what sad siege weather matrix buffalo state shoot", "06992419a8fe821dd8de03d4c300614e8feefb5ea936b76f89976dcace8aebee"),
190            ("leader monkey parrot ring guide accident before fence cannon height naive bean", "7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a"),
191            ("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"),
192        ];
193
194        for (mnemonic, expected_secret_key) in list.into_iter() {
195            let keys =
196                Keys::from_mnemonic_with_ctx(&secp, mnemonic, None, None, None, None).unwrap();
197            assert_eq!(
198                keys.secret_key(),
199                &SecretKey::from_str(expected_secret_key).unwrap()
200            );
201        }
202    }
203}