watermelon_mini/proto/
authenticator.rs1use 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 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}