lexe/types/
auth.rs

1//! Authentication, identity, and node verification.
2
3use std::{fmt, io::Write, path::Path, str::FromStr};
4
5use anyhow::Context;
6use bip39::Mnemonic;
7use lexe_common::{
8    ExposeSecret,
9    api::user::{NodePk as UnstableNodePk, UserPk as UnstableUserPk},
10    root_seed::RootSeed as UnstableRootSeed,
11};
12use lexe_crypto::rng::SysRng;
13use lexe_enclave::enclave::Measurement as UnstableMeasurement;
14use lexe_node_client::credentials::{
15    ClientCredentials as UnstableClientCredentials,
16    CredentialsRef as UnstableCredentialsRef,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::{
21    config::WalletEnv,
22    util::{ByteArray, hex},
23};
24
25// --- Credentials --- //
26
27/// Credentials used to authenticate with a Lexe user node.
28#[derive(Debug)]
29pub enum Credentials {
30    /// Authenticate with a [`RootSeed`].
31    RootSeed(RootSeed),
32    /// Authenticate with delegated [`ClientCredentials`].
33    ClientCredentials(ClientCredentials),
34}
35
36impl Credentials {
37    /// Borrow as a [`CredentialsRef`].
38    pub fn as_ref(&self) -> CredentialsRef<'_> {
39        match self {
40            Self::RootSeed(root_seed) => CredentialsRef::RootSeed(root_seed),
41            Self::ClientCredentials(cc) =>
42                CredentialsRef::ClientCredentials(cc),
43        }
44    }
45}
46
47impl From<RootSeed> for Credentials {
48    fn from(root_seed: RootSeed) -> Self {
49        Self::RootSeed(root_seed)
50    }
51}
52
53impl From<ClientCredentials> for Credentials {
54    fn from(cc: ClientCredentials) -> Self {
55        Self::ClientCredentials(cc)
56    }
57}
58
59// --- CredentialsRef --- //
60
61/// Borrowed version of [`Credentials`].
62#[derive(Copy, Clone, Debug)]
63pub enum CredentialsRef<'a> {
64    /// Authenticate with a borrowed [`RootSeed`].
65    RootSeed(&'a RootSeed),
66    /// Authenticate with borrowed [`ClientCredentials`].
67    ClientCredentials(&'a ClientCredentials),
68}
69
70impl<'a> CredentialsRef<'a> {
71    /// Returns the user public key, if available.
72    ///
73    /// Always `Some(_)` if the credentials were created by `node-v0.8.11+`.
74    pub(crate) fn user_pk(self) -> Option<UserPk> {
75        self.to_unstable().user_pk().map(UserPk::from_unstable)
76    }
77
78    /// Convert to the inner [`UnstableCredentialsRef`] used by
79    /// `lexe-node-client`.
80    pub(crate) fn to_unstable(self) -> UnstableCredentialsRef<'a> {
81        match self {
82            Self::RootSeed(root_seed) =>
83                UnstableCredentialsRef::RootSeed(root_seed.unstable()),
84            Self::ClientCredentials(cc) =>
85                UnstableCredentialsRef::ClientCredentials(cc.unstable()),
86        }
87    }
88}
89
90impl<'a> From<&'a RootSeed> for CredentialsRef<'a> {
91    fn from(root_seed: &'a RootSeed) -> Self {
92        Self::RootSeed(root_seed)
93    }
94}
95
96impl<'a> From<&'a ClientCredentials> for CredentialsRef<'a> {
97    fn from(cc: &'a ClientCredentials) -> Self {
98        Self::ClientCredentials(cc)
99    }
100}
101
102// --- RootSeed --- //
103
104/// The root secret from which the user node's keys and credentials are derived.
105#[derive(Serialize, Deserialize)]
106pub struct RootSeed(UnstableRootSeed);
107
108impl RootSeed {
109    // --- Constructors & File I/O --- //
110
111    /// Generate a new random [`RootSeed`] using the system CSPRNG.
112    pub fn generate() -> Self {
113        Self(UnstableRootSeed::from_rng(&mut SysRng::new()))
114    }
115
116    /// Read a [`RootSeed`] from the default seedphrase path for this
117    /// environment (`~/.lexe/seedphrase[.env].txt`).
118    ///
119    /// Returns `Ok(None)` if the file doesn't exist.
120    pub fn read(wallet_env: &WalletEnv) -> anyhow::Result<Option<Self>> {
121        let lexe_data_dir = lexe_common::default_lexe_data_dir()
122            .context("Could not get default lexe data dir")?;
123        let path = wallet_env.seedphrase_path(&lexe_data_dir);
124        Self::read_from_path(&path)
125    }
126
127    /// Write this [`RootSeed`] to the default seedphrase path for this
128    /// environment (`~/.lexe/seedphrase[.env].txt`).
129    ///
130    /// Creates parent directories if needed. Fails if the file already exists.
131    pub fn write(&self, wallet_env: &WalletEnv) -> anyhow::Result<()> {
132        let lexe_data_dir = lexe_common::default_lexe_data_dir()
133            .context("Could not get default lexe data dir")?;
134        let path = wallet_env.seedphrase_path(&lexe_data_dir);
135        self.write_to_path(&path)
136    }
137
138    /// Read a [`RootSeed`] from a seedphrase file at a specific path.
139    ///
140    /// Returns `Ok(None)` if the file doesn't exist.
141    pub fn read_from_path(path: &Path) -> anyhow::Result<Option<Self>> {
142        match std::fs::read_to_string(path) {
143            Ok(contents) => {
144                let mnemonic = bip39::Mnemonic::from_str(contents.trim())
145                    .map_err(|e| anyhow::anyhow!("Invalid mnemonic: {e}"))?;
146                Ok(Some(Self::try_from(mnemonic)?))
147            }
148            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
149            Err(e) => Err(e).context("Failed to read seedphrase file"),
150        }
151    }
152
153    /// Write this [`RootSeed`] to a seedphrase file at a specific path.
154    ///
155    /// Creates parent directories if needed. Returns an error if the file
156    /// already exists. On Unix, the file is created with mode 0600 (owner
157    /// read/write only).
158    pub fn write_to_path(&self, path: &Path) -> anyhow::Result<()> {
159        #[cfg(unix)]
160        use std::os::unix::fs::OpenOptionsExt;
161
162        // Ensure parent directory exists
163        if let Some(parent) = path.parent() {
164            std::fs::create_dir_all(parent)
165                .context("Failed to create data directory")?;
166        }
167
168        // Open with create_new to fail if file exists
169        let mut opts = std::fs::OpenOptions::new();
170        opts.write(true).create_new(true);
171
172        // Set restrictive permissions on Unix (owner read/write only)
173        #[cfg(unix)]
174        opts.mode(0o600);
175
176        let mut file = opts.open(path).with_context(|| {
177            format!("Seedphrase file already exists: {}", path.display())
178        })?;
179
180        let mnemonic = self.to_mnemonic();
181        writeln!(file, "{mnemonic}")
182            .context("Failed to write seedphrase file")?;
183
184        tracing::info!("Persisted seedphrase to {}", path.display());
185
186        Ok(())
187    }
188
189    /// Construct a [`RootSeed`] from a BIP39 mnemonic.
190    pub fn from_mnemonic(mnemonic: Mnemonic) -> anyhow::Result<Self> {
191        Self::try_from(mnemonic)
192    }
193
194    /// Construct a [`RootSeed`] from a 32-byte slice.
195    pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
196        Self::try_from(bytes)
197    }
198
199    /// Construct a [`RootSeed`] from a 64-character hex string.
200    pub fn from_hex(hex: &str) -> anyhow::Result<Self> {
201        Self::from_str(hex).map_err(anyhow::Error::from)
202    }
203
204    // --- Serialization --- //
205
206    /// Convert this root secret to its BIP39 mnemonic.
207    pub fn to_mnemonic(&self) -> Mnemonic {
208        self.unstable().to_mnemonic()
209    }
210
211    /// Borrow the 32-byte root secret.
212    pub fn as_bytes(&self) -> &[u8] {
213        self.unstable().expose_secret()
214    }
215
216    /// Encode the root secret as a 64-character hex string.
217    pub fn to_hex(&self) -> String {
218        hex::encode(self.as_bytes())
219    }
220
221    // --- Derived Identity --- //
222    /// Derive the user's public key.
223    pub fn derive_user_pk(&self) -> UserPk {
224        UserPk::from_unstable(self.unstable().derive_user_pk())
225    }
226
227    /// Derive the node public key.
228    pub fn derive_node_pk(&self) -> NodePk {
229        NodePk::from_unstable(self.unstable().derive_node_pk())
230    }
231
232    // --- Encryption --- //
233
234    /// Encrypt this root secret under the given password.
235    pub fn password_encrypt(&self, password: &str) -> anyhow::Result<Vec<u8>> {
236        self.unstable()
237            .password_encrypt(&mut SysRng::new(), password)
238    }
239
240    /// Decrypt a password-encrypted root secret.
241    pub fn password_decrypt(
242        password: &str,
243        encrypted: Vec<u8>,
244    ) -> anyhow::Result<Self> {
245        UnstableRootSeed::password_decrypt(password, encrypted).map(Self)
246    }
247
248    // --- Internal Escape Hatches --- //
249
250    cfg_if::cfg_if! {
251        if #[cfg(feature = "unstable")] {
252            /// Returns the wrapped internal root-seed type.
253            ///
254            /// This is only exposed when the `unstable` feature is enabled.
255            pub fn unstable(&self) -> &UnstableRootSeed {
256                &self.0
257            }
258        } else {
259            pub(crate) fn unstable(&self) -> &UnstableRootSeed {
260                &self.0
261            }
262        }
263    }
264
265    cfg_if::cfg_if! {
266        if #[cfg(feature = "unstable")] {
267            /// Destructure this SDK root seed into the internal root-seed type.
268            ///
269            /// This is only exposed when the `unstable` feature is enabled.
270            pub fn into_unstable(self) -> UnstableRootSeed {
271                self.0
272            }
273        } else {
274            pub(crate) fn into_unstable(self) -> UnstableRootSeed {
275                self.0
276            }
277        }
278    }
279}
280
281impl fmt::Debug for RootSeed {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        fmt::Debug::fmt(self.unstable(), f)
284    }
285}
286
287impl FromStr for RootSeed {
288    type Err = <UnstableRootSeed as FromStr>::Err;
289
290    fn from_str(s: &str) -> Result<Self, Self::Err> {
291        UnstableRootSeed::from_str(s).map(Self)
292    }
293}
294
295impl TryFrom<&[u8]> for RootSeed {
296    type Error = anyhow::Error;
297
298    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
299        UnstableRootSeed::try_from(bytes).map(Self)
300    }
301}
302
303impl TryFrom<Mnemonic> for RootSeed {
304    type Error = anyhow::Error;
305
306    fn try_from(mnemonic: Mnemonic) -> Result<Self, Self::Error> {
307        UnstableRootSeed::try_from(mnemonic).map(Self)
308    }
309}
310
311// --- ClientCredentials --- //
312
313/// Scoped and revocable credentials for controlling a Lexe user node.
314///
315/// These are useful when you want node access without exposing the user's
316/// [`RootSeed`], which is irrevocable.
317#[derive(Clone)]
318pub struct ClientCredentials(UnstableClientCredentials);
319
320impl ClientCredentials {
321    /// Parse [`ClientCredentials`] from a string.
322    pub fn from_string(s: &str) -> anyhow::Result<Self> {
323        Self::from_str(s)
324    }
325
326    /// Export these credentials as a portable string.
327    ///
328    /// The returned string can be passed to [`ClientCredentials::from_string`]
329    /// to reconstruct the credentials.
330    pub fn export_string(&self) -> String {
331        self.unstable().to_base64_blob()
332    }
333
334    /// Access the inner [`UnstableClientCredentials`].
335    pub(crate) fn unstable(&self) -> &UnstableClientCredentials {
336        &self.0
337    }
338}
339
340impl FromStr for ClientCredentials {
341    type Err = anyhow::Error;
342
343    fn from_str(s: &str) -> Result<Self, Self::Err> {
344        UnstableClientCredentials::try_from_base64_blob(s).map(Self)
345    }
346}
347
348impl fmt::Debug for ClientCredentials {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        fmt::Debug::fmt(&self.0, f)
351    }
352}
353
354// --- Measurement --- //
355
356/// An SGX enclave measurement (MRENCLAVE).
357///
358/// This is the hash of the enclave binary, used to verify that a node is
359/// running a specific version. Returned in
360/// [`NodeInfo`](super::command::NodeInfo).
361///
362/// The measurement is a 32-byte value typically represented as a 64-character
363/// hex string.
364///
365/// Implements [`ByteArray<32>`].
366#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
367#[serde(transparent)]
368pub struct Measurement(UnstableMeasurement);
369
370impl ByteArray<32> for Measurement {
371    fn from_array(array: [u8; 32]) -> Self {
372        Self(UnstableMeasurement::from_array(array))
373    }
374    fn to_array(&self) -> [u8; 32] {
375        self.0.to_array()
376    }
377    fn as_array(&self) -> &[u8; 32] {
378        self.0.as_array()
379    }
380}
381
382impl AsRef<[u8]> for Measurement {
383    fn as_ref(&self) -> &[u8] {
384        self.0.as_array().as_slice()
385    }
386}
387
388impl AsRef<[u8; 32]> for Measurement {
389    fn as_ref(&self) -> &[u8; 32] {
390        self.0.as_array()
391    }
392}
393
394impl fmt::Debug for Measurement {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        fmt::Debug::fmt(&self.0, f)
397    }
398}
399
400impl fmt::Display for Measurement {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        fmt::Display::fmt(&self.0, f)
403    }
404}
405
406impl FromStr for Measurement {
407    type Err = <UnstableMeasurement as FromStr>::Err;
408
409    fn from_str(s: &str) -> Result<Self, Self::Err> {
410        UnstableMeasurement::from_str(s).map(Self)
411    }
412}
413
414impl Measurement {
415    /// Destructure this SDK type into the internal type.
416    #[cfg(feature = "unstable")]
417    pub fn unstable(self) -> UnstableMeasurement {
418        self.0
419    }
420
421    cfg_if::cfg_if! {
422        if #[cfg(feature = "unstable")] {
423            /// Wraps an internal type into this SDK type.
424            pub fn from_unstable(inner: UnstableMeasurement) -> Self {
425                Self(inner)
426            }
427        } else {
428            pub(crate) fn from_unstable(inner: UnstableMeasurement) -> Self {
429                Self(inner)
430            }
431        }
432    }
433}
434
435#[cfg(feature = "unstable")]
436impl From<UnstableMeasurement> for Measurement {
437    fn from(inner: UnstableMeasurement) -> Self {
438        Self(inner)
439    }
440}
441
442#[cfg(feature = "unstable")]
443impl From<Measurement> for UnstableMeasurement {
444    fn from(outer: Measurement) -> Self {
445        outer.0
446    }
447}
448
449// --- UserPk --- //
450
451/// A Lexe user's primary identifier, derived from the root seed.
452///
453/// Serialized as a 64-character hex string (32 bytes).
454///
455/// Implements [`ByteArray<32>`].
456#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
457#[serde(transparent)]
458pub struct UserPk(UnstableUserPk);
459
460impl ByteArray<32> for UserPk {
461    fn from_array(array: [u8; 32]) -> Self {
462        Self(UnstableUserPk::from_array(array))
463    }
464    fn to_array(&self) -> [u8; 32] {
465        self.0.to_array()
466    }
467    fn as_array(&self) -> &[u8; 32] {
468        self.0.as_array()
469    }
470}
471
472impl AsRef<[u8]> for UserPk {
473    fn as_ref(&self) -> &[u8] {
474        self.0.as_array().as_slice()
475    }
476}
477
478impl AsRef<[u8; 32]> for UserPk {
479    fn as_ref(&self) -> &[u8; 32] {
480        self.0.as_array()
481    }
482}
483
484impl fmt::Debug for UserPk {
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        fmt::Debug::fmt(&self.0, f)
487    }
488}
489
490impl fmt::Display for UserPk {
491    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492        fmt::Display::fmt(&self.0, f)
493    }
494}
495
496impl FromStr for UserPk {
497    type Err = <UnstableUserPk as FromStr>::Err;
498
499    fn from_str(s: &str) -> Result<Self, Self::Err> {
500        UnstableUserPk::from_str(s).map(Self)
501    }
502}
503
504impl UserPk {
505    cfg_if::cfg_if! {
506        if #[cfg(feature = "unstable")] {
507            /// Destructure this SDK type into the internal type.
508            pub fn unstable(self) -> UnstableUserPk {
509                self.0
510            }
511        } else {
512            pub(crate) fn unstable(self) -> UnstableUserPk {
513                self.0
514            }
515        }
516    }
517
518    cfg_if::cfg_if! {
519        if #[cfg(feature = "unstable")] {
520            /// Wraps an internal type into this SDK type.
521            pub fn from_unstable(inner: UnstableUserPk) -> Self {
522                Self(inner)
523            }
524        } else {
525            pub(crate) fn from_unstable(inner: UnstableUserPk) -> Self {
526                Self(inner)
527            }
528        }
529    }
530}
531
532#[cfg(feature = "unstable")]
533impl From<UnstableUserPk> for UserPk {
534    fn from(inner: UnstableUserPk) -> Self {
535        Self(inner)
536    }
537}
538
539#[cfg(feature = "unstable")]
540impl From<UserPk> for UnstableUserPk {
541    fn from(outer: UserPk) -> Self {
542        outer.0
543    }
544}
545
546// --- NodePk --- //
547
548/// A Lightning node's secp256k1 public key (the `node_id`).
549///
550/// Serialized as a 66-character hex string (33 bytes, compressed).
551#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
552#[serde(transparent)]
553pub struct NodePk(UnstableNodePk);
554
555impl NodePk {
556    /// Construct from a 66-character hex string.
557    pub fn from_hex(hex_str: &str) -> anyhow::Result<Self> {
558        Self::from_str(hex_str).map_err(anyhow::Error::from)
559    }
560
561    /// Encode as a 66-character hex string.
562    pub fn to_hex(&self) -> String {
563        self.to_string()
564    }
565}
566
567impl fmt::Debug for NodePk {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        fmt::Debug::fmt(&self.0, f)
570    }
571}
572
573impl fmt::Display for NodePk {
574    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575        fmt::Display::fmt(&self.0, f)
576    }
577}
578
579impl FromStr for NodePk {
580    type Err = <UnstableNodePk as FromStr>::Err;
581
582    fn from_str(s: &str) -> Result<Self, Self::Err> {
583        UnstableNodePk::from_str(s).map(Self)
584    }
585}
586
587impl NodePk {
588    /// Destructure this SDK type into the internal type.
589    #[cfg(feature = "unstable")]
590    pub fn unstable(self) -> UnstableNodePk {
591        self.0
592    }
593
594    cfg_if::cfg_if! {
595        if #[cfg(feature = "unstable")] {
596            /// Wraps an internal type into this SDK type.
597            pub fn from_unstable(inner: UnstableNodePk) -> Self {
598                Self(inner)
599            }
600        } else {
601            pub(crate) fn from_unstable(inner: UnstableNodePk) -> Self {
602                Self(inner)
603            }
604        }
605    }
606}
607
608#[cfg(feature = "unstable")]
609impl From<UnstableNodePk> for NodePk {
610    fn from(inner: UnstableNodePk) -> Self {
611        Self(inner)
612    }
613}
614
615#[cfg(feature = "unstable")]
616impl From<NodePk> for UnstableNodePk {
617    fn from(outer: NodePk) -> Self {
618        outer.0
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    #[test]
627    fn seedphrase_file_roundtrip() {
628        let root_seed1 = RootSeed::generate();
629
630        let tempdir = tempfile::tempdir().unwrap();
631        let path = tempdir.path().join("seedphrase.txt");
632
633        // Write seedphrase to file
634        root_seed1.write_to_path(&path).unwrap();
635
636        // Read it back
637        let root_seed2 = RootSeed::read_from_path(&path).unwrap().unwrap();
638        assert_eq!(root_seed1.as_bytes(), root_seed2.as_bytes());
639
640        // Writing again should fail (file exists)
641        let err = root_seed1.write_to_path(&path).unwrap_err();
642        assert!(err.to_string().contains("already exists"));
643
644        // Reading non-existent file should return None
645        let missing = tempdir.path().join("missing.txt");
646        assert!(RootSeed::read_from_path(&missing).unwrap().is_none());
647    }
648}