watermelon_mini/proto/
authenticator.rs

1use std::fmt::{self, Debug, Formatter};
2
3use watermelon_nkeys::{KeyPair, KeyPairFromSeedError};
4use watermelon_proto::{Connect, ServerAddr, ServerInfo};
5
6pub enum AuthenticationMethod {
7    UserAndPassword { username: String, password: String },
8    Creds { jwt: String, nkey: KeyPair },
9}
10
11#[derive(Debug, thiserror::Error)]
12pub enum AuthenticationError {
13    #[error("missing nonce")]
14    MissingNonce,
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum CredsParseError {
19    #[error("contents are truncated")]
20    Truncated,
21    #[error("missing closing for JWT")]
22    MissingJwtClosing,
23    #[error("missing closing for nkey")]
24    MissingNkeyClosing,
25    #[error("missing JWT")]
26    MissingJwt,
27    #[error("missing nkey")]
28    MissingNkey,
29    #[error("invalid nkey")]
30    InvalidKey(#[source] KeyPairFromSeedError),
31}
32
33impl AuthenticationMethod {
34    pub(crate) fn try_from_addr(addr: &ServerAddr) -> Option<Self> {
35        if let (Some(username), Some(password)) = (addr.username(), addr.password()) {
36            Some(Self::UserAndPassword {
37                username: username.to_owned(),
38                password: password.to_owned(),
39            })
40        } else {
41            None
42        }
43    }
44
45    pub(crate) fn prepare_for_auth(
46        &self,
47        info: &ServerInfo,
48        connect: &mut Connect,
49    ) -> Result<(), AuthenticationError> {
50        match self {
51            Self::UserAndPassword { username, password } => {
52                connect.username = Some(username.clone());
53                connect.password = Some(password.clone());
54            }
55            Self::Creds { jwt, nkey } => {
56                let nonce = info
57                    .nonce
58                    .as_deref()
59                    .ok_or(AuthenticationError::MissingNonce)?;
60                let signature = nkey.sign(nonce.as_bytes()).to_string();
61
62                connect.jwt = Some(jwt.clone());
63                connect.nkey = Some(nkey.public_key().to_string());
64                connect.signature = Some(signature);
65            }
66        }
67
68        Ok(())
69    }
70
71    /// Creates an `AuthenticationMethod` from the content of a credentials file.
72    ///
73    /// # Errors
74    ///
75    /// It returns an error if the content is not valid.
76    pub fn from_creds(contents: &str) -> Result<Self, CredsParseError> {
77        let mut jtw = None;
78        let mut secret = None;
79
80        let mut lines = contents.lines();
81        while let Some(line) = lines.next() {
82            if line == "-----BEGIN NATS USER JWT-----" {
83                jtw = Some(lines.next().ok_or(CredsParseError::Truncated)?);
84
85                let line = lines.next().ok_or(CredsParseError::Truncated)?;
86                if line != "------END NATS USER JWT------" {
87                    return Err(CredsParseError::MissingJwtClosing);
88                }
89            } else if line == "-----BEGIN USER NKEY SEED-----" {
90                secret = Some(lines.next().ok_or(CredsParseError::Truncated)?);
91
92                let line = lines.next().ok_or(CredsParseError::Truncated)?;
93                if line != "------END USER NKEY SEED------" {
94                    return Err(CredsParseError::MissingNkeyClosing);
95                }
96            }
97        }
98
99        let jtw = jtw.ok_or(CredsParseError::MissingJwt)?;
100        let nkey = secret.ok_or(CredsParseError::MissingNkey)?;
101        let nkey = KeyPair::from_encoded_seed(nkey).map_err(CredsParseError::InvalidKey)?;
102
103        Ok(Self::Creds {
104            jwt: jtw.to_owned(),
105            nkey,
106        })
107    }
108}
109
110impl Debug for AuthenticationMethod {
111    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
112        f.debug_struct("AuthenticationMethod")
113            .finish_non_exhaustive()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::AuthenticationMethod;
120
121    #[test]
122    fn parse_creds() {
123        let creds = r"-----BEGIN NATS USER JWT-----
124eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJUVlNNTEtTWkJBN01VWDNYQUxNUVQzTjRISUw1UkZGQU9YNUtaUFhEU0oyWlAzNkVMNVJBIiwiaWF0IjoxNTU4MDQ1NTYyLCJpc3MiOiJBQlZTQk0zVTQ1REdZRVVFQ0tYUVM3QkVOSFdHN0tGUVVEUlRFSEFKQVNPUlBWV0JaNEhPSUtDSCIsIm5hbWUiOiJvbWVnYSIsInN1YiI6IlVEWEIyVk1MWFBBU0FKN1pEVEtZTlE3UU9DRldTR0I0Rk9NWVFRMjVIUVdTQUY3WlFKRUJTUVNXIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.6TQ2ilCDb6m2ZDiJuj_D_OePGXFyN3Ap2DEm3ipcU5AhrWrNvneJryWrpgi_yuVWKo1UoD5s8bxlmwypWVGFAA
125------END NATS USER JWT------
126
127************************* IMPORTANT *************************
128NKEY Seed printed below can be used to sign and prove identity.
129NKEYs are sensitive and should be treated as secrets.
130
131-----BEGIN USER NKEY SEED-----
132SUAOY5JZ2WJKVR4UO2KJ2P3SW6FZFNWEOIMAXF4WZEUNVQXXUOKGM55CYE
133------END USER NKEY SEED------
134
135*************************************************************";
136
137        let AuthenticationMethod::Creds { jwt, nkey } =
138            AuthenticationMethod::from_creds(creds).unwrap()
139        else {
140            panic!("invalid auth method");
141        };
142        assert_eq!(
143            jwt,
144            "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJUVlNNTEtTWkJBN01VWDNYQUxNUVQzTjRISUw1UkZGQU9YNUtaUFhEU0oyWlAzNkVMNVJBIiwiaWF0IjoxNTU4MDQ1NTYyLCJpc3MiOiJBQlZTQk0zVTQ1REdZRVVFQ0tYUVM3QkVOSFdHN0tGUVVEUlRFSEFKQVNPUlBWV0JaNEhPSUtDSCIsIm5hbWUiOiJvbWVnYSIsInN1YiI6IlVEWEIyVk1MWFBBU0FKN1pEVEtZTlE3UU9DRldTR0I0Rk9NWVFRMjVIUVdTQUY3WlFKRUJTUVNXIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.6TQ2ilCDb6m2ZDiJuj_D_OePGXFyN3Ap2DEm3ipcU5AhrWrNvneJryWrpgi_yuVWKo1UoD5s8bxlmwypWVGFAA"
145        );
146        assert_eq!(
147            nkey.public_key().to_string(),
148            "SAAO4HKVRO54CIBH7EONLBWD6BYIW2IYHQVZTCCDLU6C2IAX7GBEQGJDYE"
149        );
150    }
151}