Skip to main content

lexe_common/
root_seed.rs

1use std::{fmt, num::NonZeroU32, str::FromStr};
2
3use anyhow::{Context, bail, ensure};
4use bitcoin::{
5    bip32::{self, ChildNumber},
6    secp256k1,
7};
8use lexe_byte_array::ByteArray;
9use lexe_crypto::{
10    aes::{self, AesMasterKey},
11    ed25519, entropy, hmac, password,
12    rng::{Crng, RngExt},
13};
14use lexe_hex::hex;
15use lexe_std::array;
16use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize};
17use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
18
19use crate::{
20    api::user::{NodePk, UserPk},
21    ln::network::Network,
22    secp256k1_ctx::SECP256K1,
23};
24
25// TODO(phlip9): [perf] consider storing extracted `Prk` alongside seed to
26//               reduce key derivation time by ~60-70% : )
27
28/// The user's 32-byte root seed, from which all keys and credentials are
29/// derived (user keypair, node keypair, TLS certificates, etc.).
30// We intentionally don't implement ByteArray because it makes it too easy
31// to access the secret.
32pub struct RootSeed(Secret<[u8; Self::LENGTH]>);
33
34impl RootSeed {
35    pub const LENGTH: usize = 32;
36
37    /// An HKDF can't extract more than `255 * hash_output_size` bytes for a
38    /// single secret.
39    const HKDF_MAX_OUT_LEN: usize = 8160 /* 255*32 */;
40
41    /// We salt the HKDF for domain separation purposes.
42    const HKDF_SALT: [u8; 32] = array::pad(*b"LEXE-REALM::RootSeed");
43
44    /// Buffer size for writing a BIP39 mnemonic sentence.
45    /// 24 words * max 8 chars + 23 spaces = 215 <= 216 bytes max
46    const BIP39_MNEMONIC_BUF_SIZE: usize = 216;
47
48    pub fn new(bytes: Secret<[u8; Self::LENGTH]>) -> Self {
49        Self(bytes)
50    }
51
52    /// Quickly create a `RootSeed` for tests.
53    #[cfg(any(test, feature = "test-utils"))]
54    pub fn from_u64(v: u64) -> Self {
55        let mut seed = [0u8; 32];
56        seed[0..8].copy_from_slice(&v.to_le_bytes());
57        Self::new(Secret::new(seed))
58    }
59
60    /// Sample directly from `rng` for tests.
61    #[cfg(any(test, feature = "test-utils"))]
62    pub fn from_rng<R: Crng>(rng: &mut R) -> Self {
63        Self(Secret::new(rng.gen_bytes()))
64    }
65
66    /// Generate a root seed with [`entropy::gen_seed`], which mixes in extra
67    /// entropy.
68    ///
69    /// Always non-deterministic, so prefer `RootSeed::from_rng` in tests.
70    pub fn from_rng_with_extra_entropy<R: Crng>(rng: &mut R) -> Self {
71        Self(Secret::new(entropy::gen_seed(rng)))
72    }
73
74    // --- BIP39 Mnemonics --- //
75
76    /// Creates a [`bip39::Mnemonic`] from this [`RootSeed`]. Use
77    /// [`bip39::Mnemonic`]'s `Display` / `FromStr` impls to convert from / to
78    /// user-facing strings.
79    pub fn to_mnemonic(&self) -> bip39::Mnemonic {
80        bip39::Mnemonic::from_entropy_in(
81            bip39::Language::English,
82            self.0.expose_secret().as_slice(),
83        )
84        .expect("Always succeeds for 256 bits")
85    }
86
87    /// Derives the BIP39-compatible 64-byte seed from this [`RootSeed`].
88    ///
89    /// This uses the standard BIP39 derivation:
90    /// `PBKDF2(password=mnemonic, salt="mnemonic", 2048 rounds, HMAC-SHA512)`
91    ///
92    /// The resulting seed is compatible with standard wallets when used to
93    /// derive a BIP32 master xpriv.
94    ///
95    /// New Lexe wallets created > node-v0.9.1 use this to derive their
96    /// on-chain wallet BIP32 master xprivs.
97    ///
98    /// Old Lexe on-chain wallets use the [`Self::derive_legacy_master_xprv`]
99    /// instead.
100    pub fn derive_bip39_seed(&self) -> Secret<[u8; 64]> {
101        // RootSeed ("entropy") -> mnemonic
102        let mnemonic = self.to_mnemonic();
103
104        // Write out mnemonic words separated by spaces. Do it on the stack
105        // to avoid allocations.
106        let mut buf = [0u8; Self::BIP39_MNEMONIC_BUF_SIZE];
107        let mut len = 0;
108        for (i, word) in mnemonic.words().enumerate() {
109            if i > 0 {
110                buf[len] = b' ';
111                len += 1;
112            }
113            let word_bytes = word.as_bytes();
114            buf[len..len + word_bytes.len()].copy_from_slice(word_bytes);
115            len += word_bytes.len();
116        }
117        let mnemonic_bytes = &buf[..len];
118
119        // BIP39 salt is "mnemonic" + passphrase (empty for standard wallets)
120        let salt = b"mnemonic";
121
122        // mnemonic -- PBKDF2 -> BIP39 seed
123        let mut seed = [0u8; 64];
124        ring::pbkdf2::derive(
125            ring::pbkdf2::PBKDF2_HMAC_SHA512,
126            const { NonZeroU32::new(2048).unwrap() },
127            salt,
128            mnemonic_bytes,
129            &mut seed,
130        );
131
132        // Zeroize the temporary buffer
133        buf.zeroize();
134
135        Secret::new(seed)
136    }
137
138    // --- Key derivations --- //
139
140    fn extract(&self) -> ring::hkdf::Prk {
141        let salted_hkdf = ring::hkdf::Salt::new(
142            ring::hkdf::HKDF_SHA256,
143            Self::HKDF_SALT.as_slice(),
144        );
145        salted_hkdf.extract(self.0.expose_secret().as_slice())
146    }
147
148    /// Derive a new child secret with `label` into a prepared buffer `out`.
149    pub fn derive_to_slice(&self, label: &[&[u8]], out: &mut [u8]) {
150        struct OkmLength(usize);
151
152        impl ring::hkdf::KeyType for OkmLength {
153            fn len(&self) -> usize {
154                self.0
155            }
156        }
157
158        assert!(out.len() <= Self::HKDF_MAX_OUT_LEN);
159
160        self.extract()
161            .expand(label, OkmLength(out.len()))
162            .expect("should not fail")
163            .fill(out)
164            .expect("should not fail")
165    }
166
167    /// Derive a new child secret with `label` to a hash-output-sized buffer.
168    pub fn derive(&self, label: &[&[u8]]) -> Secret<[u8; 32]> {
169        let mut out = [0u8; 32];
170        self.derive_to_slice(label, &mut out);
171        Secret::new(out)
172    }
173
174    /// Convenience method to derive a new child secret with `label` into a
175    /// `Vec<u8>` of size `out_len`.
176    pub fn derive_vec(&self, label: &[&[u8]], out_len: usize) -> SecretVec<u8> {
177        let mut out = vec![0u8; out_len];
178        self.derive_to_slice(label, &mut out);
179        SecretVec::new(out)
180    }
181
182    /// Derive the keypair for the "ephemeral issuing" CA that endorses
183    /// client and server certs under the "shared seed" mTLS construction.
184    pub fn derive_ephemeral_issuing_ca_key_pair(&self) -> ed25519::KeyPair {
185        // TODO(max): Ideally rename to "ephemeral issuing ca key pair", but
186        // need to ensure backwards compatibility. Both client and server need
187        // to trust the old + new CAs before the old CA can be removed.
188        let seed = self.derive(&[b"shared seed tls ca key pair"]);
189        ed25519::KeyPair::from_seed(seed.expose_secret())
190    }
191
192    /// Derive the keypair for the "revocable issuing" CA that endorses
193    /// client and server certs under the "shared seed" mTLS construction.
194    pub fn derive_revocable_issuing_ca_key_pair(&self) -> ed25519::KeyPair {
195        let seed = self.derive(&[b"revocable issuing ca key pair"]);
196        ed25519::KeyPair::from_seed(seed.expose_secret())
197    }
198
199    /// Derive the user key pair, which is the key behind the [`UserPk`]. This
200    /// key pair is also used to sign up and authenticate as the user against
201    /// the lexe backend.
202    ///
203    /// [`UserPk`]: crate::api::user::UserPk
204    pub fn derive_user_key_pair(&self) -> ed25519::KeyPair {
205        let seed = self.derive(&[b"user key pair"]);
206        ed25519::KeyPair::from_seed(seed.expose_secret())
207    }
208
209    /// Convenience function to derive the [`UserPk`].
210    pub fn derive_user_pk(&self) -> UserPk {
211        UserPk::new(self.derive_user_key_pair().public_key().to_array())
212    }
213
214    /// Derive the BIP32 master xpriv using the BIP39-compatible derived 64-byte
215    /// seed.
216    ///
217    /// This is used for new Lexe on-chain wallets created > node-v0.9.1.
218    /// Wallets created before then use the [`Self::derive_legacy_master_xprv`].
219    ///
220    /// This produces keys compatible with standard wallets that follow the
221    /// BIP39 spec.
222    pub fn derive_bip32_master_xprv(&self, network: Network) -> bip32::Xpriv {
223        let bip39_seed = self.derive_bip39_seed();
224        bip32::Xpriv::new_master(
225            network.to_bitcoin(),
226            bip39_seed.expose_secret(),
227        )
228        .expect("Should never fail")
229    }
230
231    /// Derive the "legacy" BIP32 master xpriv by feeding the 32-byte
232    /// [`RootSeed`] directly into BIP32's HMAC-SHA512.
233    ///
234    /// This is used for LDK seed derivation (via [`Self::derive_ldk_seed`]) and
235    /// for existing on-chain wallets created before BIP39 compatibility.
236    ///
237    /// It's called "legacy" because standard BIP39 wallets derive the master
238    /// xpriv from a 64-byte seed (produced by PBKDF2), not the original 32-byte
239    /// entropy. This makes Lexe's old on-chain addresses incompatible with
240    /// external wallets. New on-chain wallets use the BIP39-compatible
241    /// derivation instead.
242    pub fn derive_legacy_master_xprv(&self, network: Network) -> bip32::Xpriv {
243        bip32::Xpriv::new_master(network.to_bitcoin(), self.0.expose_secret())
244            .expect("Should never fail")
245    }
246
247    /// Derives the root seed used in LDK. The `KeysManager` is initialized
248    /// using this seed, and `secp256k1` keys are derived from this seed.
249    pub fn derive_ldk_seed(&self) -> Secret<[u8; 32]> {
250        // The [u8; 32] output will be the same regardless of the network the
251        // master_xprv uses, as tested in `when_does_network_matter`
252        let master_xprv = self.derive_legacy_master_xprv(Network::Mainnet);
253
254        // Derive the hardened child key at `m/535h`, where 535 is T9 for "LDK"
255        let m_535h =
256            ChildNumber::from_hardened_idx(535).expect("Is within [0, 2^31-1]");
257        let ldk_xprv = master_xprv
258            .derive_priv(&SECP256K1, &m_535h)
259            .expect("Should always succeed");
260
261        Secret::new(ldk_xprv.private_key.secret_bytes())
262    }
263
264    /// Derive the Lightning node key pair without needing to derive all the
265    /// other auxiliary node secrets used in the `KeysManager`.
266    pub fn derive_node_key_pair(&self) -> secp256k1::Keypair {
267        // Derive the LDK seed first.
268        let ldk_seed = self.derive_ldk_seed();
269
270        // When deriving a secp256k1 key, the network doesn't matter.
271        // This is checked in when_does_network_matter.
272        let ldk_xprv = bip32::Xpriv::new_master(
273            bitcoin::Network::Bitcoin,
274            ldk_seed.expose_secret(),
275        )
276        .expect("should never fail; the sizes match up");
277
278        let m_0h = ChildNumber::from_hardened_idx(0)
279            .expect("should never fail; index is in range");
280        let node_sk = ldk_xprv
281            .derive_priv(&SECP256K1, &m_0h)
282            .expect("should never fail")
283            .private_key;
284
285        secp256k1::Keypair::from_secret_key(&SECP256K1, &node_sk)
286    }
287
288    /// Convenience function to derive the Lightning node pubkey.
289    pub fn derive_node_pk(&self) -> NodePk {
290        NodePk(self.derive_node_key_pair().public_key())
291    }
292
293    /// A secret key used by LDK to authenticate message contexts in received
294    /// `BlindedMessagePath`s.
295    ///
296    /// Used within LDK to create BOLT12 offers with a `BlindedMessagePath`.
297    ///
298    /// This method lets us derive this key without needing to derive all the
299    /// other auxiliary node secrets used in the LDK `KeysManager`.
300    //
301    // See: <https://github.com/lightningdevkit/rust-lightning/blob/714777567be2cfc3dc3a041fcaaff2a7f75b533c/lightning/src/sign/mod.rs#L2015>
302    // for how this is derived upstream.
303    #[cfg(any(test, feature = "test-utils"))]
304    pub fn derive_receive_auth_key(&self) -> [u8; 32] {
305        // Derive the LDK seed first.
306        let ldk_seed = self.derive_ldk_seed();
307
308        // When deriving a secp256k1 key, the network doesn't matter.
309        // This is checked in when_does_network_matter.
310        let ldk_xprv = bip32::Xpriv::new_master(
311            bitcoin::Network::Bitcoin,
312            ldk_seed.expose_secret(),
313        )
314        .expect("should never fail; the sizes match up");
315
316        let m_7h = ChildNumber::from_hardened_idx(7)
317            .expect("should never fail; index is in range");
318        let sk = ldk_xprv
319            .derive_priv(&SECP256K1, &m_7h)
320            .expect("should never fail")
321            .private_key;
322
323        sk.secret_bytes()
324    }
325
326    pub fn derive_vfs_master_key(&self) -> AesMasterKey {
327        let secret = self.derive(&[b"vfs master key"]);
328        AesMasterKey::new(secret.expose_secret())
329    }
330
331    /// Derive the VSS auth signing key for a particulr provider by hostname.
332    ///
333    /// If we just use one VSS auth key for all providers, then one provider
334    /// can reuse the same token to read/write/delete files at another
335    /// provider.
336    pub fn derive_vss_auth_key(&self, hostname: &str) -> secp256k1::SecretKey {
337        let secret = self.derive(&[b"vss auth key\0", hostname.as_bytes()]);
338        secp256k1::SecretKey::from_slice(secret.expose_secret())
339            .expect("HKDF output should be a valid secp256k1 secret key")
340    }
341
342    /// Derive the key used to authenticate "continuations": server-issued
343    /// opaque blobs that a client echoes back to us (e.g. a signed
344    /// LDK route).
345    pub fn derive_continuation_mac_key(&self) -> hmac::Key {
346        let secret = self.derive(&[b"continuation mac key"]);
347        hmac::Key::from_seed(secret.expose_secret())
348    }
349
350    #[cfg(any(test, feature = "test-utils"))]
351    pub fn as_bytes(&self) -> &[u8] {
352        self.0.expose_secret().as_slice()
353    }
354
355    // --- Password encryption --- //
356
357    /// Attempts to encrypt this root seed under the given password.
358    ///
359    /// The password must have at least [`MIN_PASSWORD_LENGTH`] characters and
360    /// must not have any more than [`MAX_PASSWORD_LENGTH`] characters.
361    ///
362    /// Returns a [`Vec<u8>`] which can be persisted and later decrypted using
363    /// only the given password.
364    ///
365    /// [`MIN_PASSWORD_LENGTH`]: lexe_crypto::password::MIN_PASSWORD_LENGTH
366    /// [`MAX_PASSWORD_LENGTH`]: lexe_crypto::password::MAX_PASSWORD_LENGTH
367    pub fn password_encrypt(
368        &self,
369        rng: &mut impl Crng,
370        password: &str,
371    ) -> anyhow::Result<Vec<u8>> {
372        // Sample a completely random salt for maximum security.
373        let salt = rng.gen_bytes();
374
375        // Obtain the password-encrypted AES ciphertext.
376        let mut aes_ciphertext =
377            password::encrypt(rng, password, &salt, self.0.expose_secret())
378                .context("Password encryption failed")?;
379
380        // Final persistable value is `salt || aes_ciphertext`
381        let mut combined = Vec::from(salt);
382        combined.append(&mut aes_ciphertext);
383
384        // Sanity check the length of the combined salt + aes_ciphertext.
385        // Combined length is 32 bytes (salt) + encrypted length of 32 byte seed
386        let expected_combined_len = 32 + aes::encrypted_len(32);
387        assert!(combined.len() == expected_combined_len);
388
389        Ok(combined)
390    }
391
392    /// Attempts to construct a [`RootSeed`] given a decryption password and the
393    /// [`Vec<u8>`] returned from a previous call to [`password_encrypt`].
394    ///
395    /// [`password_encrypt`]: Self::password_encrypt
396    pub fn password_decrypt(
397        password: &str,
398        mut combined: Vec<u8>,
399    ) -> anyhow::Result<Self> {
400        // Combined length is 32 bytes (salt) + encrypted length of 32 byte seed
401        let expected_combined_len = 32 + aes::encrypted_len(32);
402        ensure!(
403            combined.len() == expected_combined_len,
404            "Combined bytes had the wrong length"
405        );
406
407        // Split `salt || aes_ciphertext` into component parts
408        let aes_ciphertext = combined.split_off(32);
409        let unsized_salt = combined.into_boxed_slice();
410        let salt = Box::<[u8; 32]>::try_from(unsized_salt)
411            .expect("We split off at 32, so there are exactly 32 bytes");
412
413        // Password-decrypt.
414        let root_seed_bytes =
415            password::decrypt(password, &salt, aes_ciphertext)
416                .map(Secret::new)
417                .context("Password decryption failed")?;
418
419        // Construct the RootSeed
420        Self::try_from(root_seed_bytes.expose_secret().as_slice())
421    }
422}
423
424impl ExposeSecret<[u8; Self::LENGTH]> for RootSeed {
425    fn expose_secret(&self) -> &[u8; Self::LENGTH] {
426        self.0.expose_secret()
427    }
428}
429
430impl FromStr for RootSeed {
431    type Err = hex::DecodeError;
432
433    fn from_str(hex: &str) -> Result<Self, Self::Err> {
434        let mut bytes = [0u8; Self::LENGTH];
435        hex::decode_to_slice(hex, bytes.as_mut_slice())
436            .map(|()| Self::new(Secret::new(bytes)))
437    }
438}
439
440impl fmt::Debug for RootSeed {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        // Avoid formatting secrets.
443        f.write_str("RootSeed(..)")
444    }
445}
446
447impl TryFrom<&[u8]> for RootSeed {
448    type Error = anyhow::Error;
449
450    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
451        if bytes.len() != Self::LENGTH {
452            bail!("input must be {} bytes", Self::LENGTH);
453        }
454        let mut out = [0u8; Self::LENGTH];
455        out[..].copy_from_slice(bytes);
456        Ok(Self::new(Secret::new(out)))
457    }
458}
459
460impl TryFrom<bip39::Mnemonic> for RootSeed {
461    type Error = anyhow::Error;
462
463    fn try_from(mnemonic: bip39::Mnemonic) -> Result<Self, Self::Error> {
464        use lexe_std::array::ArrayExt;
465
466        // to_entropy_array() returns [u8; 33]
467        let (entropy, entropy_len) = mnemonic.to_entropy_array();
468        let entropy = secrecy::zeroize::Zeroizing::new(entropy);
469
470        ensure!(entropy_len == 32, "Should contain exactly 32 bytes");
471
472        let (seed_buf, _remainder) = entropy.split_array_ref_stable::<32>();
473
474        Ok(Self(Secret::new(*seed_buf)))
475    }
476}
477
478struct RootSeedVisitor;
479
480impl de::Visitor<'_> for RootSeedVisitor {
481    type Value = RootSeed;
482
483    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
484        f.write_str("hex-encoded RootSeed or raw bytes")
485    }
486
487    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
488    where
489        E: de::Error,
490    {
491        RootSeed::from_str(v).map_err(serde::de::Error::custom)
492    }
493
494    fn visit_bytes<E>(self, b: &[u8]) -> Result<Self::Value, E>
495    where
496        E: de::Error,
497    {
498        RootSeed::try_from(b).map_err(de::Error::custom)
499    }
500}
501
502impl<'de> Deserialize<'de> for RootSeed {
503    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
504    where
505        D: Deserializer<'de>,
506    {
507        if deserializer.is_human_readable() {
508            deserializer.deserialize_str(RootSeedVisitor)
509        } else {
510            deserializer.deserialize_bytes(RootSeedVisitor)
511        }
512    }
513}
514
515impl Serialize for RootSeed {
516    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
517    where
518        S: Serializer,
519    {
520        if serializer.is_human_readable() {
521            let hex_str = hex::encode(self.0.expose_secret());
522            serializer.serialize_str(&hex_str)
523        } else {
524            serializer.serialize_bytes(self.0.expose_secret())
525        }
526    }
527}
528
529#[cfg(any(test, feature = "test-utils"))]
530mod test_impls {
531    use proptest::{
532        arbitrary::{Arbitrary, any},
533        strategy::{BoxedStrategy, Strategy},
534    };
535
536    use super::*;
537
538    impl Arbitrary for RootSeed {
539        type Strategy = BoxedStrategy<Self>;
540        type Parameters = ();
541
542        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
543            any::<[u8; 32]>()
544                .prop_map(|buf| Self::new(Secret::new(buf)))
545                .no_shrink()
546                .boxed()
547        }
548    }
549
550    // only impl PartialEq in tests; not safe to compare root seeds w/o constant
551    // time comparison.
552    impl PartialEq for RootSeed {
553        fn eq(&self, other: &Self) -> bool {
554            self.expose_secret() == other.expose_secret()
555        }
556    }
557}
558
559#[cfg(test)]
560mod test {
561    use std::path::Path;
562
563    use bitcoin::NetworkKind;
564    use lexe_crypto::rng::FastRng;
565    use lexe_sha256::sha256;
566    use proptest::{
567        arbitrary::any, collection::vec, prop_assert_eq, proptest,
568        strategy::Strategy, test_runner::Config,
569    };
570
571    use super::*;
572    use crate::ln::network::Network;
573
574    // simple implementations of some crypto functions for equivalence testing
575
576    // an inefficient impl of HMAC-SHA256 for equivalence testing
577    fn hmac_sha256(key: &[u8], msg: &[u8]) -> sha256::Hash {
578        let h_key = sha256::digest(key);
579        let mut zero_pad_key = [0u8; 64];
580
581        // make key match the internal block size
582        let key = match key.len() {
583            len if len > 64 => h_key.as_ref(),
584            _ => key,
585        };
586        zero_pad_key[..key.len()].copy_from_slice(key);
587        let key = zero_pad_key.as_slice();
588        assert_eq!(key.len(), 64);
589
590        // o_key := [ key_i ^ 0x5c ]_{i in 0..64}
591        let mut o_key = [0u8; 64];
592        for (o_key_i, key_i) in o_key.iter_mut().zip(key) {
593            *o_key_i = key_i ^ 0x5c;
594        }
595
596        // i_key := [ key_i ^ 0x36 ]_{i in 0..64}
597        let mut i_key = [0u8; 64];
598        for (i_key_i, key_i) in i_key.iter_mut().zip(key) {
599            *i_key_i = key_i ^ 0x36;
600        }
601
602        // h_i := H(i_key || msg)
603        let h_i = sha256::digest_many(&[&i_key, msg]);
604
605        // output := H(o_key || H(i_key || msg))
606        sha256::digest_many(&[&o_key, h_i.as_ref()])
607    }
608
609    // an inefficient impl of HKDF-SHA256 for equivalence testing
610    fn hkdf_sha256(
611        ikm: &[u8],
612        salt: &[u8],
613        info: &[&[u8]],
614        out_len: usize,
615    ) -> Vec<u8> {
616        let prk = hmac_sha256(salt, ikm);
617
618        // N := ceil(out_len / block_size)
619        //   := (out_len.saturating_sub(1) / block_size) + 1
620        let n = (out_len.saturating_sub(1) / 32) + 1;
621        let n = u8::try_from(n).expect("out_len too large");
622
623        // T := T(1) | T(2) | .. | T(N)
624        // T(0) := b"" (empty byte string)
625        // T(i+1) := hmac_sha256(prk, T(i) || info || [ i+1 ])
626
627        let mut t_i = [0u8; 32];
628        let mut out = Vec::new();
629
630        for i in 1..=n {
631            // m_i := T(i-1) || info || [ i ]
632            let mut m_i = if i == 1 { Vec::new() } else { t_i.to_vec() };
633            for info_part in info {
634                m_i.extend_from_slice(info_part);
635            }
636            m_i.extend_from_slice(&[i]);
637
638            let h_i = hmac_sha256(prk.as_ref(), &m_i);
639            t_i.copy_from_slice(h_i.as_ref());
640
641            if i < n {
642                out.extend_from_slice(&t_i[..]);
643            } else {
644                let l = 32 - (((n as usize) * 32) - out_len);
645                out.extend_from_slice(&t_i[..l]);
646            }
647        }
648
649        out
650    }
651
652    /// ```bash
653    /// $ cargo test -p common -- dump_root_seed --ignored --show-output
654    /// ```
655    #[ignore]
656    #[test]
657    fn dump_root_seed() {
658        let root_seed = RootSeed::from_u64(20240506);
659        let root_seed_hex = hex::encode(root_seed.expose_secret());
660        let user_pk = root_seed.derive_user_pk();
661        let node_pk = root_seed.derive_node_pk();
662
663        println!(
664            "root_seed: '{root_seed_hex}', \
665             user_pk: '{user_pk}', node_pk: '{node_pk}'"
666        );
667    }
668
669    #[test]
670    fn test_root_seed_serde() {
671        let input =
672            "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069";
673        let input_json = format!("\"{input}\"");
674        let seed_bytes = hex::decode(input).unwrap();
675
676        let seed = RootSeed::from_str(input).unwrap();
677        assert_eq!(seed.as_bytes(), &seed_bytes);
678
679        let seed2: RootSeed = serde_json::from_str(&input_json).unwrap();
680        assert_eq!(seed2.as_bytes(), &seed_bytes);
681
682        #[derive(Deserialize)]
683        struct Foo {
684            x: u32,
685            seed: RootSeed,
686            y: String,
687        }
688
689        let foo_json = format!(
690            "{{\n\
691            \"x\": 123,\n\
692            \"seed\": \"{input}\",\n\
693            \"y\": \"asdf\"\n\
694        }}"
695        );
696
697        let foo2: Foo = serde_json::from_str(&foo_json).unwrap();
698        assert_eq!(foo2.x, 123);
699        assert_eq!(foo2.seed.as_bytes(), &seed_bytes);
700        assert_eq!(foo2.y, "asdf");
701    }
702
703    #[test]
704    fn test_root_seed_derive() {
705        let seed = RootSeed::from_u64(0x42);
706
707        let out8 = seed.derive_vec(&[b"very cool secret"], 8);
708        let out16 = seed.derive_vec(&[b"very cool secret"], 16);
709        let out32 = seed.derive_vec(&[b"very cool secret"], 32);
710        let out32_2 = seed.derive(&[b"very cool secret"]);
711
712        assert_eq!("c724f46ae4c48017", hex::encode(out8.expose_secret()));
713        assert_eq!(
714            "c724f46ae4c480172a75cf775dbb64b1",
715            hex::encode(out16.expose_secret())
716        );
717        assert_eq!(
718            "c724f46ae4c480172a75cf775dbb64b160beb74137eb7d0cef72fde0523674de",
719            hex::encode(out32.expose_secret())
720        );
721        assert_eq!(out32.expose_secret(), out32_2.expose_secret());
722    }
723
724    // Fuzz our KDF against a basic, readable implementation of HKDF-SHA256.
725    #[test]
726    fn test_root_seed_derive_equiv() {
727        let arb_seed = any::<RootSeed>();
728        let arb_label = vec(vec(any::<u8>(), 0..=64), 0..=4);
729        let arb_len = 0_usize..=1024;
730
731        proptest!(|(seed in arb_seed, label in arb_label, len in arb_len)| {
732            let label = label
733                .iter()
734                .map(|x| x.as_slice())
735                .collect::<Vec<_>>();
736
737            let expected = hkdf_sha256(
738                seed.as_bytes(),
739                RootSeed::HKDF_SALT.as_slice(),
740                &label,
741                len,
742            );
743
744            let actual = seed.derive_vec(&label, len);
745
746            assert_eq!(&expected, actual.expose_secret());
747        });
748    }
749
750    /// A series of tests that demonstrate when the [`Network`] affects the
751    /// partial equality of key material at various stages of derivation.
752    /// This helps determine whether our APIs should take a [`Network`] as a
753    /// parameter, or if setting a default would be sufficient.
754    #[test]
755    fn when_does_network_matter() {
756        proptest!(|(
757            root_seed in any::<RootSeed>(),
758            network1 in any::<Network>(),
759            network2 in any::<Network>(),
760        )| {
761            let network_kind1 = NetworkKind::from(network1.to_bitcoin());
762            let network_kind2 = NetworkKind::from(network2.to_bitcoin());
763
764            // Network DOES matter for master xprvs (and all xprvs in general),
765            // but only to the extent that their `NetworkKind` is different.
766            // i.e. a `Signet` and `Testnet` xprv may be considered the same.
767            let master_xprv1 = root_seed.derive_legacy_master_xprv(network1);
768            let master_xprv2 = root_seed.derive_legacy_master_xprv(network2);
769            // Assert: "master xprvs are equal iff network kinds are equal"
770            let master_xprvs_equal = master_xprv1 == master_xprv2;
771            let network_kinds_equal = network_kind1 == network_kind2;
772            prop_assert_eq!(master_xprvs_equal, network_kinds_equal);
773
774            // Test derive_ldk_seed(): The [u8; 32] LDK seed should be the same
775            // regardless of the network of the master_xprv it was based on
776            let m_535h = ChildNumber::from_hardened_idx(535)
777                .expect("Is within [0, 2^31-1]");
778            let ldk_seed1 = master_xprv1
779                .derive_priv(&SECP256K1, &m_535h)
780                .expect("Should always succeed")
781                .private_key
782                .secret_bytes();
783            let ldk_seed2 = master_xprv2
784                .derive_priv(&SECP256K1, &m_535h)
785                .expect("Should always succeed")
786                .private_key
787                .secret_bytes();
788            prop_assert_eq!(ldk_seed1, ldk_seed2);
789            let ldk_seed = ldk_seed1;
790
791            // Test derive_node_key_pair() and derive_node_pk(): The outputted
792            // secp256k1::Keypair should be the same regardless of the network
793            // of the ldk_xprv it was based on
794            let ldk_xprv1 = bip32::Xpriv::new_master(network1.to_bitcoin(), &ldk_seed)
795                .expect("Should never fail");
796            let ldk_xprv2 = bip32::Xpriv::new_master(network2.to_bitcoin(), &ldk_seed)
797                .expect("Should never fail");
798            // Assert: "ldk_xprvs are equal iff network kinds are equal"
799            let ldk_xprvs_equal = ldk_xprv1 == ldk_xprv2;
800            prop_assert_eq!(ldk_xprvs_equal, network_kinds_equal);
801            // First check the node_sks
802            let m_0h = ChildNumber::from_hardened_idx(0)
803                .expect("should never fail; index is in range");
804            let node_sk1 = ldk_xprv1
805                .derive_priv(&SECP256K1, &m_0h)
806                .expect("should never fail")
807                .private_key;
808            let node_sk2 = ldk_xprv2
809                .derive_priv(&SECP256K1, &m_0h)
810                .expect("should never fail")
811                .private_key;
812            prop_assert_eq!(node_sk1, node_sk2);
813            // Then check the keypairs
814            let keypair1 =
815                secp256k1::Keypair::from_secret_key(&SECP256K1, &node_sk1);
816            let keypair2 =
817                secp256k1::Keypair::from_secret_key(&SECP256K1, &node_sk2);
818            prop_assert_eq!(keypair1, keypair2);
819            // Then check the node_pks
820            let node_pk1 = NodePk(secp256k1::PublicKey::from(keypair1));
821            let node_pk2 = NodePk(secp256k1::PublicKey::from(keypair2));
822            prop_assert_eq!(node_pk1, node_pk2);
823            // Then check the serialized node_pks
824            let node_pk1_str = node_pk1.to_string();
825            let node_pk2_str = node_pk2.to_string();
826            prop_assert_eq!(node_pk1_str, node_pk2_str);
827        });
828    }
829
830    #[test]
831    fn password_encryption_roundtrip() {
832        use password::{MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH};
833
834        let password_length_range = MIN_PASSWORD_LENGTH..MAX_PASSWORD_LENGTH;
835        let any_valid_password =
836            proptest::collection::vec(any::<char>(), password_length_range)
837                .prop_map(String::from_iter);
838
839        // Reduce cases since we do key stretching which is quite expensive
840        let config = Config::with_cases(4);
841        proptest!(config, |(
842            mut rng in any::<FastRng>(),
843            password in any_valid_password,
844        )| {
845            let root_seed1 = RootSeed::from_rng(&mut rng);
846            let encrypted = root_seed1.password_encrypt(&mut rng, &password)
847                .unwrap();
848            let root_seed2 = RootSeed::password_decrypt(&password, encrypted)
849                .unwrap();
850            assert_eq!(root_seed1, root_seed2);
851        })
852    }
853
854    #[test]
855    fn password_decryption_compatibility() {
856        let root_seed1 = RootSeed::new(Secret::new([69u8; 32]));
857        let password1 = "password1234";
858        // // Uncomment to regenerate
859        // let mut rng = FastRng::from_u64(20231017);
860        // let encrypted =
861        //     root_seed1.password_encrypt(&mut rng, password1).unwrap();
862        // let encrypted_hex = hex::display(&encrypted);
863        // println!("Encrypted: {encrypted_hex}");
864
865        let encrypted = hex::decode("adcfc4aef26858bacfae83dd19e735bb145203ab18183cbe932cd742b4446e7300b561678b0652666b316288bbb57552c4f40e91d8e440fd1085cba610204ca982f52fce471de27fe360e9560cee0996e55ce7ac323201908b7ff261b8ff425a87d215e83870e45062d988627c8cb7216b").unwrap();
866        let root_seed1_decrypted =
867            RootSeed::password_decrypt(password1, encrypted).unwrap();
868        assert_eq!(root_seed1, root_seed1_decrypted);
869
870        let root_seed2 = RootSeed::new(Secret::new([0u8; 32]));
871        let password2 = "                ";
872        // // Uncomment to regenerate
873        // let mut rng = FastRng::from_u64(20231017);
874        // let encrypted =
875        //     root_seed2.password_encrypt(&mut rng, password2).unwrap();
876        // let encrypted_hex = hex::display(&encrypted);
877        // println!("Encrypted: {encrypted_hex}");
878
879        let encrypted = hex::decode("adcfc4aef26858bacfae83dd19e735bb145203ab18183cbe932cd742b4446e7300b561678b0652666b316288bbb57552c4f40e91d8e440fd1085cba610204ca982062fbcb21c14cdb9d107f2f359e0f272e473d2cdb71a870d8fb19d1169c160876ee1ccde4f73a8f2b4ebc9bed68f6139").unwrap();
880        let root_seed2_decrypted =
881            RootSeed::password_decrypt(password2, encrypted).unwrap();
882        assert_eq!(root_seed2, root_seed2_decrypted);
883    }
884
885    #[test]
886    fn root_seed_mnemonic_round_trip() {
887        proptest!(|(root_seed1 in any::<RootSeed>())| {
888            let mnemonic = root_seed1.to_mnemonic();
889
890            // All mnemonics should have exactly 24 words.
891            prop_assert_eq!(mnemonic.word_count(), 24);
892
893            let root_seed2 = RootSeed::try_from(mnemonic).unwrap();
894            prop_assert_eq!(
895                root_seed1.expose_secret(), root_seed2.expose_secret()
896            );
897        });
898    }
899
900    /// Check correctness of `bip39::Mnemonic`'s `FromStr` / `Display` impls
901    #[test]
902    fn mnemonic_fromstr_display_roundtrip() {
903        proptest!(|(root_seed in any::<RootSeed>())| {
904            let mnemonic1 = root_seed.to_mnemonic();
905            let mnemonic2 = bip39::Mnemonic::from_str(&mnemonic1.to_string()).unwrap();
906            prop_assert_eq!(mnemonic1, mnemonic2)
907        })
908    }
909
910    /// A basic compatibility test to check that a few "known good" pairings of
911    /// [`RootSeed`] <-> [`Mnemonic`] <-> [`String`] still correspond. This
912    /// ensures that the [`bip39`] crate cannot introduce compatibility-breaking
913    /// changes without us noticing.
914    #[test]
915    fn mnemonic_compatibility_test() {
916        // This code generated the "known good" values
917        // let mut rng = FastRng::from_u64(98592174);
918        // let seed1 = RootSeed::from_rng(&mut rng);
919        // let seed2 = RootSeed::from_rng(&mut rng);
920        // let seed3 = RootSeed::from_rng(&mut rng);
921        // let seed1_str = hex::encode(seed1.as_bytes());
922        // let seed2_str = hex::encode(seed2.as_bytes());
923        // let seed3_str = hex::encode(seed3.as_bytes());
924        // println!("{seed1_str}");
925        // println!("{seed2_str}");
926        // println!("{seed3_str}");
927        // let mnenemenmenomic1 = seed1.to_mnemonic().to_string();
928        // let mnenemenmenomic2 = seed2.to_mnemonic().to_string();
929        // let mnenemenmenomic3 = seed3.to_mnemonic().to_string();
930        // println!("{mnenemenmenomic1}");
931        // println!("{mnenemenmenomic2}");
932        // println!("{mnenemenmenomic3}");
933
934        // "Known good" seeds
935        let seed1 = RootSeed::new(Secret::new(hex::decode_const(
936            b"91f24ce8326abc2e9faef6a3b866021ce9574c11210e86b0f457a31ed8ad4cba",
937        )));
938        let seed2 = RootSeed::new(Secret::new(hex::decode_const(
939            b"5c2aa5fdd678112c8b13d745b5c1d1e1a81ace76721ec72f1424bd2eb387a8af",
940        )));
941        let seed3 = RootSeed::new(Secret::new(hex::decode_const(
942            b"51ddba4775fc71fb1dba65dfc2ffab7526dd61bae7a9b13e9f3aa550bee19360",
943        )));
944
945        // "Known good" mnemonic strings
946        let str1 = String::from(
947            "music mystery deliver gospel profit blanket leaf tell \
948            photo segment letter degree nice plastic duty canyon \
949            mammal marble bicycle economy unique find cream dune",
950        );
951        let str2 = String::from(
952            "found festival legal provide library north clump kit \
953            east puppy inner select like grunt supply duck \
954            shrimp judge ankle kid twenty sense pencil tray",
955        );
956        let str3 = String::from(
957            "fade universe mushroom typical shove work ivory erosion \
958            thank blood turn tumble horse radio twist vivid \
959            raise visual solid enjoy armor ignore eternal arrange",
960        );
961
962        // Check `Mnemonic`
963        let mnemonic_from_str1 = bip39::Mnemonic::from_str(&str1).unwrap();
964        let mnemonic_from_str2 = bip39::Mnemonic::from_str(&str2).unwrap();
965        let mnemonic_from_str3 = bip39::Mnemonic::from_str(&str3).unwrap();
966        assert_eq!(seed1.to_mnemonic(), mnemonic_from_str1);
967        assert_eq!(seed2.to_mnemonic(), mnemonic_from_str2);
968        assert_eq!(seed3.to_mnemonic(), mnemonic_from_str3);
969
970        // Check `RootSeed`
971        let seed_from_str1 =
972            RootSeed::try_from(mnemonic_from_str1.clone()).unwrap();
973        let seed_from_str2 =
974            RootSeed::try_from(mnemonic_from_str2.clone()).unwrap();
975        let seed_from_str3 =
976            RootSeed::try_from(mnemonic_from_str3.clone()).unwrap();
977        assert_eq!(seed1.as_bytes(), seed_from_str1.as_bytes());
978        assert_eq!(seed2.as_bytes(), seed_from_str2.as_bytes());
979        assert_eq!(seed3.as_bytes(), seed_from_str3.as_bytes());
980
981        // Check `String`
982        assert_eq!(str1, seed1.to_mnemonic().to_string());
983        assert_eq!(str2, seed2.to_mnemonic().to_string());
984        assert_eq!(str3, seed3.to_mnemonic().to_string());
985    }
986
987    /// Snapshot tests to ensure key derivations don't change.
988    /// These protect backwards compatibility for existing wallets.
989    #[test]
990    fn derive_snapshots() {
991        let seed = RootSeed::from_u64(20240506);
992
993        // Lexe user pubkey
994        let user_pk = seed.derive_user_pk();
995        assert_eq!(
996            user_pk.to_string(),
997            "a9edf9596ddf589918beca32d148a7d0ba59273b419ccf63a910f1b75861ff06",
998        );
999
1000        // Lightning node pubkey
1001        let node_pk = seed.derive_node_pk();
1002        assert_eq!(
1003            node_pk.to_string(),
1004            "035a70d45eec7efb270319f116a9684250acb4ef282a26d21874878e7c5088f73b",
1005        );
1006
1007        // VSS authentication keys
1008        let lexe_vss_key = seed.derive_vss_auth_key("vss.lexe.app");
1009        assert_eq!(
1010            hex::encode(&lexe_vss_key.secret_bytes()),
1011            "29646f43becb6541b08dcc018911fc268826d13808080870739be43c9acac923",
1012        );
1013        let external_vss_key = seed.derive_vss_auth_key("vss.example");
1014        assert_eq!(
1015            hex::encode(&external_vss_key.secret_bytes()),
1016            "a10dd5d440f79f87d64526402772b2fcc97687f110cf79e224eca16605c7386f",
1017        );
1018
1019        // LDK seed (used to initialize KeysManager)
1020        let ldk_seed = seed.derive_ldk_seed();
1021        assert_eq!(
1022            hex::encode(ldk_seed.expose_secret()),
1023            "551444699ae8acbebe67d5b54da844e8297b83e26e205203a65f29564eaf3787",
1024        );
1025
1026        // BIP39 compatible 64-byte seed
1027        let bip39_seed = seed.derive_bip39_seed();
1028        assert_eq!(
1029            hex::encode(bip39_seed.expose_secret()),
1030            "30dc1cca6811e6f52a6efba751db4fe9495883b778c72b28ee248f0076cf03b9\
1031             dc3c3d7d662c98806ce59c0e59911a249533ca0c82dea3780cdf040f9a3dfe09",
1032        );
1033
1034        // BIP39-compatible master xpriv (for new on-chain wallets)
1035        let bip39_master_xpriv =
1036            seed.derive_bip32_master_xprv(Network::Mainnet);
1037        assert_eq!(
1038            bip39_master_xpriv.to_string(),
1039            "xprv9s21ZrQH143K3BwTSDGEpsQA99b5fmckcX2s4dBbxojs287ApWXGThVTu9\
1040             TmogYG8A1JiUnbD6gHSfw5hXsTduny878ygutaCaCvg1KTvgM",
1041        );
1042
1043        // BIP39-compatible master xpriv (Testnet)
1044        let bip39_testnet_xpriv =
1045            seed.derive_bip32_master_xprv(Network::Testnet3);
1046        assert_eq!(
1047            bip39_testnet_xpriv.to_string(),
1048            "tprv8ZgxMBicQKsPe1Az6n7jzX29TH1HuHekx4wyw3c4SnELoirFoss1ySrupK\
1049             dRp3vaVbY5iaQMNTG5uXUppkDQSy4ZekMHMGcd7fxM7h7WWqo"
1050        );
1051
1052        // Legacy Lexe master xpriv (used for existing on-chain wallets)
1053        let master_xpriv = seed.derive_legacy_master_xprv(Network::Mainnet);
1054        assert_eq!(
1055            master_xpriv.to_string(),
1056            "xprv9s21ZrQH143K42JPXVa2Q7nAp6XB3FVwyYdGkQetMYRcprZXKvt52p1tqg\
1057             9fwyFJaL6Ki92bCdRNDPAnyddy7CzpQAEktM8nMtNGw4Xj6vt",
1058        );
1059
1060        // Legacy Lexe master xpriv (Testnet)
1061        let master_xpriv_testnet =
1062            seed.derive_legacy_master_xprv(Network::Testnet3);
1063        assert_eq!(
1064            master_xpriv_testnet.to_string(),
1065            "tprv8ZgxMBicQKsPeqXvC4RXZmQA8DwPGmXxK6YPcq5LqWv6cTJcKJDpYZPLk\
1066             rKKxLdcwmd6iEeMMz1AgEiY6qyuvGGQvoT4YhrqGz7hNoR5R4G",
1067        );
1068
1069        // Ephemeral issuing CA pubkey
1070        let ephemeral_ca = seed.derive_ephemeral_issuing_ca_key_pair();
1071        assert_eq!(
1072            ephemeral_ca.public_key().to_string(),
1073            "70656b5a6084c457bf004dad264cecc131879b7e6791fe0cc828c38cc0df6e92",
1074        );
1075
1076        // Revocable issuing CA pubkey
1077        let revocable_ca = seed.derive_revocable_issuing_ca_key_pair();
1078        assert_eq!(
1079            revocable_ca.public_key().to_string(),
1080            "efe6e020ba9ca4a50467cdbaff469f9d465f21d1c6fe976868a20d97bbaa2ee3",
1081        );
1082
1083        // VFS master key (via derivation + encryption)
1084        let vfs_ctxt = seed.derive_vfs_master_key().encrypt(
1085            &mut FastRng::from_u64(1234),
1086            &[],
1087            None,
1088            &|out: &mut Vec<u8>| out.extend_from_slice(b"test"),
1089        );
1090        assert_eq!(
1091            hex::encode(&vfs_ctxt),
1092            "0000a7e6a0514440b57fcf6df97b46132adde062f1a5a224aacf4fa0f286b4c56\
1093             fe2768b7dad22333936638c5734f0d529a74880aa",
1094        );
1095    }
1096
1097    /// Verify the BIP39 mnemonic buffer size constant is large enough.
1098    #[test]
1099    fn bip39_mnemonic_buf_size() {
1100        let words = bip39::Language::English.word_list();
1101        let max_word_len = words.iter().map(|w| w.len()).max().unwrap();
1102        assert_eq!(max_word_len, 8);
1103
1104        let root_seed = RootSeed::from_u64(20240506);
1105        let mnemonic = root_seed.to_mnemonic();
1106        let num_words = mnemonic.words().count();
1107        assert_eq!(num_words, 24);
1108
1109        // Max size: 24 words * 8 chars + 23 spaces = 215 bytes
1110        assert!(
1111            (max_word_len + 1) * num_words <= RootSeed::BIP39_MNEMONIC_BUF_SIZE
1112        );
1113    }
1114
1115    /// Verify our BIP39 seed derivation matches the rust-bip39 crate.
1116    #[test]
1117    fn derive_bip39_seed_matches_rust_bip39() {
1118        proptest!(|(root_seed in any::<RootSeed>())| {
1119            let mnemonic = root_seed.to_mnemonic();
1120
1121            // Our implementation
1122            let our_seed = root_seed.derive_bip39_seed();
1123
1124            // rust-bip39 implementation (empty passphrase)
1125            let their_seed = mnemonic.to_seed_normalized("");
1126
1127            prop_assert_eq!(our_seed.expose_secret(), &their_seed);
1128        });
1129    }
1130
1131    // ```bash
1132    // $ nix shell .#secretctl
1133    // $ PASSWORD=".." IN_PATH=".." \
1134    //     cargo test -p lexe-common --lib -- test_decrypt_root_seed --nocapture --ignored
1135    // ```
1136    #[test]
1137    #[ignore]
1138    fn test_decrypt_root_seed() {
1139        let password = std::env::var("PASSWORD").expect("`$PASSWORD` not set");
1140        let in_path = std::env::var_os("IN_PATH").expect("`$IN_PATH` not set");
1141        let in_path = Path::new(&in_path);
1142
1143        let ciphertext = std::fs::read(in_path).unwrap();
1144        let root_seed = RootSeed::password_decrypt(&password, ciphertext)
1145            .expect("Failed to decrypt");
1146
1147        let root_seed_bytes = root_seed.expose_secret().as_slice();
1148        let mut root_seed_hex = hex::encode(root_seed_bytes);
1149        println!("{root_seed_hex}");
1150
1151        root_seed_hex.zeroize();
1152    }
1153}