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