1use crate::base64;
8use hmac::{Hmac, Mac};
9use md5::Md5;
10use rustlavel_core::{Error, Result};
11use sha2::{Digest, Sha256};
12
13type HmacSha256 = Hmac<Sha256>;
14
15pub fn md5_password(user: &str, password: &str, salt: &[u8; 4]) -> String {
17 let inner = hex(&{
18 let mut hasher = Md5::new();
19 hasher.update(password.as_bytes());
20 hasher.update(user.as_bytes());
21 hasher.finalize()
22 });
23
24 let outer = hex(&{
25 let mut hasher = Md5::new();
26 hasher.update(inner.as_bytes());
27 hasher.update(salt);
28 hasher.finalize()
29 });
30
31 format!("md5{outer}")
32}
33
34fn hex(bytes: &[u8]) -> String {
35 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
36}
37
38pub struct Scram {
44 password: String,
45 client_nonce: String,
46 client_first_bare: String,
48 server_signature: Option<Vec<u8>>,
49}
50
51impl Scram {
52 pub const MECHANISM: &'static str = "SCRAM-SHA-256";
53
54 pub fn new(password: &str, nonce: String) -> Self {
60 Scram::with_username(password, "", nonce)
61 }
62
63 pub fn with_username(password: &str, username: &str, nonce: String) -> Self {
68 Scram {
69 password: password.to_string(),
70 client_first_bare: format!("n={username},r={nonce}"),
71 client_nonce: nonce,
72 server_signature: None,
73 }
74 }
75
76 pub fn client_first(&self) -> String {
78 format!("n,,{}", self.client_first_bare)
79 }
80
81 pub fn client_final(&mut self, server_first: &str) -> Result<String> {
83 let attributes = parse_attributes(server_first);
84
85 let combined_nonce = attributes
86 .iter()
87 .find(|(key, _)| *key == 'r')
88 .map(|(_, value)| value.clone())
89 .ok_or_else(|| Error::Protocol("SCRAM server-first has no nonce".into()))?;
90
91 if !combined_nonce.starts_with(&self.client_nonce) {
94 return Err(Error::Protocol("SCRAM server did not echo the client nonce".into()));
95 }
96
97 let salt = attributes
98 .iter()
99 .find(|(key, _)| *key == 's')
100 .and_then(|(_, value)| base64::decode(value))
101 .ok_or_else(|| Error::Protocol("SCRAM server-first has no salt".into()))?;
102
103 let iterations: u32 = attributes
104 .iter()
105 .find(|(key, _)| *key == 'i')
106 .and_then(|(_, value)| value.parse().ok())
107 .ok_or_else(|| Error::Protocol("SCRAM server-first has no iteration count".into()))?;
108
109 let salted = pbkdf2_sha256(self.password.as_bytes(), &salt, iterations);
110 let client_key = hmac(&salted, b"Client Key");
111 let stored_key = Sha256::digest(&client_key);
112
113 let client_final_without_proof = format!("c=biws,r={combined_nonce}");
115 let auth_message =
116 format!("{},{server_first},{client_final_without_proof}", self.client_first_bare);
117
118 let client_signature = hmac(&stored_key, auth_message.as_bytes());
119 let proof: Vec<u8> = client_key
120 .iter()
121 .zip(client_signature.iter())
122 .map(|(key, signature)| key ^ signature)
123 .collect();
124
125 let server_key = hmac(&salted, b"Server Key");
126 self.server_signature = Some(hmac(&server_key, auth_message.as_bytes()));
127
128 Ok(format!("{client_final_without_proof},p={}", base64::encode(&proof)))
129 }
130
131 pub fn verify(&self, server_final: &str) -> Result<()> {
134 let expected = self
135 .server_signature
136 .as_ref()
137 .ok_or_else(|| Error::Protocol("SCRAM finished before it started".into()))?;
138
139 let attributes = parse_attributes(server_final);
140
141 if let Some((_, message)) = attributes.iter().find(|(key, _)| *key == 'e') {
142 return Err(Error::msg(format!("authentication failed: {message}")));
143 }
144
145 let received = attributes
146 .iter()
147 .find(|(key, _)| *key == 'v')
148 .and_then(|(_, value)| base64::decode(value))
149 .ok_or_else(|| Error::Protocol("SCRAM server-final has no verifier".into()))?;
150
151 if received != *expected {
152 return Err(Error::msg(
153 "the database server failed SCRAM verification; it may not be the server you think it is"
154 .to_string(),
155 ));
156 }
157
158 Ok(())
159 }
160}
161
162fn parse_attributes(message: &str) -> Vec<(char, String)> {
164 message
165 .split(',')
166 .filter_map(|part| {
167 let mut chars = part.chars();
168 let key = chars.next()?;
169 let rest = chars.as_str();
170 rest.strip_prefix('=').map(|value| (key, value.to_string()))
171 })
172 .collect()
173}
174
175fn hmac(key: &[u8], message: &[u8]) -> Vec<u8> {
176 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
177 mac.update(message);
178 mac.finalize().into_bytes().to_vec()
179}
180
181fn pbkdf2_sha256(password: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
183 let mut salted = Vec::with_capacity(salt.len() + 4);
184 salted.extend_from_slice(salt);
185 salted.extend_from_slice(&1u32.to_be_bytes());
186
187 let mut previous = hmac(password, &salted);
188 let mut result = previous.clone();
189
190 for _ in 1..iterations {
191 previous = hmac(password, &previous);
192 for (accumulated, block) in result.iter_mut().zip(previous.iter()) {
193 *accumulated ^= block;
194 }
195 }
196
197 result
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn md5_matches_the_documented_construction() {
206 let digest = md5_password("postgres", "secret", &[0x01, 0x02, 0x03, 0x04]);
208
209 assert!(digest.starts_with("md5"));
210 assert_eq!(digest.len(), 35);
211 assert!(digest[3..].chars().all(|c| c.is_ascii_hexdigit()));
212 }
213
214 #[test]
215 fn pbkdf2_matches_a_known_vector() {
216 let salt = base64::decode("W22ZaJ0SNY7soEsUEjb6gQ==").unwrap();
218 let salted = pbkdf2_sha256(b"pencil", &salt, 4096);
219
220 assert_eq!(base64::encode(&salted), "xKSVEDI6tPlSysH6mUQZOeeOp01r6B3fcJbodRPcYV0=");
221 }
222
223 #[test]
224 fn produces_the_rfc_7677_client_proof() {
225 let mut scram = Scram::with_username("pencil", "user", "rOprNGfwEbeRWgbNEkqO".to_string());
226 assert_eq!(scram.client_first(), "n,,n=user,r=rOprNGfwEbeRWgbNEkqO");
227
228 let server_first =
229 "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
230 let client_final = scram.client_final(server_first).unwrap();
231
232 assert_eq!(
233 client_final,
234 "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,\
235 p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="
236 );
237
238 scram.verify("v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=").unwrap();
239 }
240
241 #[test]
242 fn postgres_exchanges_leave_the_username_empty() {
243 let scram = Scram::new("pencil", "abc".to_string());
244 assert_eq!(scram.client_first(), "n,,n=,r=abc");
245 }
246
247 #[test]
248 fn rejects_a_server_that_does_not_echo_the_nonce() {
249 let mut scram = Scram::new("pencil", "mynonce".to_string());
250 let error = scram.client_final("r=someoneelse,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096").unwrap_err();
251
252 assert!(error.to_string().contains("echo the client nonce"));
253 }
254
255 #[test]
256 fn rejects_a_bad_server_signature() {
257 let mut scram = Scram::with_username("pencil", "user", "rOprNGfwEbeRWgbNEkqO".to_string());
258 scram
259 .client_final(
260 "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
261 )
262 .unwrap();
263
264 let error = scram.verify("v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").unwrap_err();
265 assert!(error.to_string().contains("failed SCRAM verification"));
266 }
267
268 #[test]
269 fn surfaces_a_server_reported_authentication_error() {
270 let mut scram = Scram::with_username("pencil", "user", "rOprNGfwEbeRWgbNEkqO".to_string());
271 scram
272 .client_final(
273 "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
274 )
275 .unwrap();
276
277 let error = scram.verify("e=invalid-proof").unwrap_err();
278 assert!(error.to_string().contains("invalid-proof"));
279 }
280}