1use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use hmac::{Hmac, Mac};
13use sha2::{Digest, Sha256};
14
15type HmacSha256 = Hmac<Sha256>;
16
17const NONCE_LEN: usize = 24;
19
20const MAX_ITERATIONS: u32 = 1_000_000;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[non_exhaustive]
30pub enum ScramError {
31 Malformed,
33 NonceMismatch,
35 TooManyIterations,
37 BadSalt,
39 ServerProofInvalid,
41}
42
43pub struct Scram {
47 client_first_bare: String,
49 client_nonce: String,
51 password: String,
52 server_key: Option<Vec<u8>>,
54 auth_message: Option<String>,
55}
56
57impl core::fmt::Debug for Scram {
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 f.debug_struct("Scram")
62 .field("client_nonce", &self.client_nonce)
63 .field("password", &"<redacted>")
64 .finish_non_exhaustive()
65 }
66}
67
68impl Scram {
69 pub fn new(username: &str, password: &str, nonce: &str) -> Self {
74 let client_nonce = sanitise_nonce(nonce);
75 let client_first_bare = alloc::format!("n={},r={client_nonce}", saslprep(username));
76 Self {
77 client_first_bare,
78 client_nonce,
79 password: password.to_string(),
80 server_key: None,
81 auth_message: None,
82 }
83 }
84
85 pub fn client_first(&self) -> Vec<u8> {
87 alloc::format!("n,,{}", self.client_first_bare).into_bytes()
88 }
89
90 pub fn client_final(&mut self, server_first: &[u8]) -> Result<Vec<u8>, ScramError> {
92 let server_first = core::str::from_utf8(server_first).map_err(|_| ScramError::Malformed)?;
93 let nonce = field(server_first, 'r').ok_or(ScramError::Malformed)?;
94 let salt = field(server_first, 's').ok_or(ScramError::Malformed)?;
95 let iterations: u32 = field(server_first, 'i')
96 .ok_or(ScramError::Malformed)?
97 .parse()
98 .map_err(|_| ScramError::Malformed)?;
99
100 if !nonce.starts_with(&self.client_nonce) || nonce == self.client_nonce {
103 return Err(ScramError::NonceMismatch);
104 }
105 if iterations == 0 || iterations > MAX_ITERATIONS {
106 return Err(ScramError::TooManyIterations);
107 }
108 let salt = base64_decode(salt).ok_or(ScramError::BadSalt)?;
109
110 let salted =
111 hi(self.password.as_bytes(), &salt, iterations).ok_or(ScramError::Malformed)?;
112 let client_key = hmac(&salted, b"Client Key").ok_or(ScramError::Malformed)?;
113 let stored_key = Sha256::digest(&client_key);
114 self.server_key = Some(hmac(&salted, b"Server Key").ok_or(ScramError::Malformed)?);
115
116 let client_final_bare = alloc::format!("c=biws,r={nonce}");
119 let auth_message = alloc::format!(
120 "{},{server_first},{client_final_bare}",
121 self.client_first_bare
122 );
123
124 let client_signature =
125 hmac(&stored_key, auth_message.as_bytes()).ok_or(ScramError::Malformed)?;
126 let proof: Vec<u8> = client_key
127 .iter()
128 .zip(client_signature.iter())
129 .map(|(key, signature)| key ^ signature)
130 .collect();
131
132 self.auth_message = Some(auth_message);
133 Ok(alloc::format!("{client_final_bare},p={}", base64_encode(&proof)).into_bytes())
134 }
135
136 pub fn verify(&self, server_final: &[u8]) -> Result<(), ScramError> {
141 let server_final = core::str::from_utf8(server_final).map_err(|_| ScramError::Malformed)?;
142 let signature = field(server_final, 'v').ok_or(ScramError::Malformed)?;
143 let (Some(server_key), Some(auth_message)) = (&self.server_key, &self.auth_message) else {
144 return Err(ScramError::Malformed);
145 };
146 let expected = hmac(server_key, auth_message.as_bytes()).ok_or(ScramError::Malformed)?;
147 let given = base64_decode(signature).ok_or(ScramError::Malformed)?;
148 if constant_time_eq(&expected, &given) {
149 Ok(())
150 } else {
151 Err(ScramError::ServerProofInvalid)
152 }
153 }
154}
155
156fn hmac(key: &[u8], message: &[u8]) -> Option<Vec<u8>> {
161 let mut mac = <HmacSha256 as Mac>::new_from_slice(key).ok()?;
162 mac.update(message);
163 Some(mac.finalize().into_bytes().to_vec())
164}
165
166fn hi(password: &[u8], salt: &[u8], iterations: u32) -> Option<Vec<u8>> {
168 let mut previous = hmac(password, &[salt, &[0, 0, 0, 1]].concat())?;
169 let mut result = previous.clone();
170 for _ in 1..iterations {
171 previous = hmac(password, &previous)?;
172 for (accumulated, round) in result.iter_mut().zip(previous.iter()) {
173 *accumulated ^= round;
174 }
175 }
176 Some(result)
177}
178
179fn field(message: &str, key: char) -> Option<&str> {
181 message
182 .split(',')
183 .find_map(|part| part.strip_prefix(key)?.strip_prefix('='))
184}
185
186fn sanitise_nonce(nonce: &str) -> String {
190 let cleaned: String = nonce
191 .chars()
192 .filter(char::is_ascii_alphanumeric)
193 .take(NONCE_LEN)
194 .collect();
195 if cleaned.is_empty() {
196 "0".to_string()
197 } else {
198 cleaned
199 }
200}
201
202fn saslprep(username: &str) -> String {
207 username.replace('=', "=3D").replace(',', "=2C")
208}
209
210fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
212 if a.len() != b.len() {
213 return false;
214 }
215 a.iter()
216 .zip(b.iter())
217 .fold(0u8, |difference, (x, y)| difference | (x ^ y))
218 == 0
219}
220
221fn base64_encode(data: &[u8]) -> String {
222 use base64::Engine as _;
223 base64::engine::general_purpose::STANDARD.encode(data)
224}
225
226fn base64_decode(text: &str) -> Option<Vec<u8>> {
227 use base64::Engine as _;
228 base64::engine::general_purpose::STANDARD.decode(text).ok()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 const USER: &str = "user";
237 const PASSWORD: &str = "pencil";
238 const CLIENT_NONCE: &str = "rOprNGfwEbeRWgbNEkqO";
239 const SERVER_FIRST: &str =
240 "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
241
242 #[test]
243 fn the_first_message_carries_the_user_and_our_nonce() {
244 let scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
245 assert_eq!(
246 scram.client_first(),
247 b"n,,n=user,r=rOprNGfwEbeRWgbNEkqO".to_vec()
248 );
249 }
250
251 #[test]
252 fn the_proof_matches_the_published_example() {
253 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
254 let final_message = scram
255 .client_final(SERVER_FIRST.as_bytes())
256 .expect("the example server message is well formed");
257 assert_eq!(
258 core::str::from_utf8(&final_message).expect("utf8"),
259 "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=",
260 "RFC 7677 publishes this exact proof, so a mismatch means the derivation is wrong"
261 );
262 }
263
264 #[test]
265 fn the_servers_own_proof_is_checked() {
266 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
267 scram
268 .client_final(SERVER_FIRST.as_bytes())
269 .expect("client final");
270 assert_eq!(
271 scram.verify(b"v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="),
272 Ok(()),
273 "the published server signature must be accepted"
274 );
275 }
276
277 #[test]
278 fn a_server_that_never_knew_the_password_is_rejected() {
279 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
280 scram
281 .client_final(SERVER_FIRST.as_bytes())
282 .expect("client final");
283 assert_eq!(
284 scram.verify(b"v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
285 Err(ScramError::ServerProofInvalid),
286 "skipping this check is how a fake server convinces a client it is real"
287 );
288 }
289
290 #[test]
291 fn a_server_that_does_not_extend_our_nonce_is_not_answering_us() {
292 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
293 assert_eq!(
294 scram.client_final(b"r=somebodyelsesnonce,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"),
295 Err(ScramError::NonceMismatch)
296 );
297 assert_eq!(
298 scram.client_final(
299 alloc::format!("r={CLIENT_NONCE},s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096").as_bytes()
300 ),
301 Err(ScramError::NonceMismatch),
302 "the server has to contribute its own half, not merely echo ours"
303 );
304 }
305
306 #[test]
307 fn an_absurd_iteration_count_is_refused_rather_than_run() {
308 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
309 assert_eq!(
310 scram.client_final(
311 alloc::format!("r={CLIENT_NONCE}x,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4000000000")
312 .as_bytes()
313 ),
314 Err(ScramError::TooManyIterations),
315 "the count comes from the server, so it must be bounded on our side"
316 );
317 assert_eq!(
318 scram.client_final(
319 alloc::format!("r={CLIENT_NONCE}x,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=0").as_bytes()
320 ),
321 Err(ScramError::TooManyIterations)
322 );
323 }
324
325 #[test]
326 fn a_malformed_server_message_is_refused() {
327 let mut scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
328 assert_eq!(scram.client_final(b""), Err(ScramError::Malformed));
329 assert_eq!(scram.client_final(b"nonsense"), Err(ScramError::Malformed));
330 assert_eq!(
331 scram.client_final(b"r=abc,s=notbase64!,i=4096"),
332 Err(ScramError::NonceMismatch),
333 "the nonce is checked before the salt is even decoded"
334 );
335 }
336
337 #[test]
338 fn verifying_before_the_exchange_has_run_fails_rather_than_passing() {
339 let scram = Scram::new(USER, PASSWORD, CLIENT_NONCE);
340 assert_eq!(scram.verify(b"v=anything"), Err(ScramError::Malformed));
341 }
342
343 #[test]
344 fn a_username_cannot_rewrite_the_message_around_it() {
345 let scram = Scram::new("ev,il=user", PASSWORD, CLIENT_NONCE);
346 let first = String::from_utf8(scram.client_first()).expect("utf8");
347 assert!(first.contains("n=ev=2Cil=3Duser"));
348 assert_eq!(
349 first.matches(',').count(),
350 3,
351 "the header's two commas plus the one before r=, and none smuggled in by the name"
352 );
353 }
354
355 #[test]
356 fn a_nonce_cannot_smuggle_a_field_separator() {
357 let scram = Scram::new(USER, PASSWORD, "abc,r=evil");
358 assert_eq!(scram.client_nonce, "abcrevil");
359 }
360
361 #[test]
362 fn an_empty_nonce_still_produces_a_usable_one() {
363 let scram = Scram::new(USER, PASSWORD, ",,,");
364 assert!(!scram.client_nonce.is_empty());
365 }
366
367 #[test]
368 fn comparison_does_not_leak_where_two_values_differ() {
369 assert!(constant_time_eq(b"abc", b"abc"));
370 assert!(!constant_time_eq(b"abc", b"abd"));
371 assert!(!constant_time_eq(b"abc", b"ab"));
372 }
373}