1#![allow(rustdoc::invalid_html_tags)]
12
13use std::fmt::Display;
14use std::num::NonZeroU32;
15
16use aws_lc_rs::constant_time::verify_slices_are_equal;
17use aws_lc_rs::digest;
18use aws_lc_rs::hmac;
19use aws_lc_rs::rand::{SecureRandom, SystemRandom};
20use base64::prelude::*;
21use itertools::Itertools;
22use mz_ore::secure::{Zeroize, Zeroizing};
23
24use crate::password::Password;
25
26const DEFAULT_SALT_SIZE: usize = 32;
28
29const SHA256_OUTPUT_LEN: usize = 32;
30
31#[derive(Debug, PartialEq)]
33pub struct HashOpts {
34 pub iterations: NonZeroU32,
36 pub salt: [u8; DEFAULT_SALT_SIZE],
40}
41
42impl Drop for HashOpts {
43 fn drop(&mut self) {
44 self.salt.zeroize();
45 }
46}
47
48pub struct PasswordHash {
49 pub salt: [u8; DEFAULT_SALT_SIZE],
51 pub iterations: NonZeroU32,
53 pub hash: [u8; SHA256_OUTPUT_LEN],
56}
57
58impl Drop for PasswordHash {
59 fn drop(&mut self) {
60 self.salt.zeroize();
61 self.hash.zeroize();
62 }
63}
64
65#[derive(Debug)]
66pub enum VerifyError {
67 MalformedHash,
68 InvalidPassword,
69 Hash(HashError),
70}
71
72#[derive(Debug)]
73pub enum HashError {
74 Crypto(aws_lc_rs::error::Unspecified),
75}
76
77impl Display for HashError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 HashError::Crypto(e) => write!(f, "crypto error: {}", e),
81 }
82 }
83}
84
85pub fn hash_password(
88 password: &Password,
89 iterations: &NonZeroU32,
90) -> Result<PasswordHash, HashError> {
91 let rng = SystemRandom::new();
92 let mut salt = Zeroizing::new([0u8; DEFAULT_SALT_SIZE]);
93 rng.fill(&mut *salt).map_err(HashError::Crypto)?;
94
95 let hash = hash_password_inner(
96 &HashOpts {
97 iterations: iterations.to_owned(),
98 salt: *salt,
99 },
100 password.as_bytes(),
101 )?;
102
103 Ok(PasswordHash {
104 salt: *salt,
105 iterations: iterations.to_owned(),
106 hash,
107 })
108}
109
110pub fn generate_nonce(client_nonce: &str) -> Result<String, HashError> {
111 let rng = SystemRandom::new();
112 let mut nonce = Zeroizing::new([0u8; 24]);
113 rng.fill(&mut *nonce).map_err(HashError::Crypto)?;
114 let nonce = BASE64_STANDARD.encode(&*nonce);
115 let new_nonce = format!("{}{}", client_nonce, nonce);
116 Ok(new_nonce)
117}
118
119pub fn hash_password_with_opts(
122 opts: &HashOpts,
123 password: &Password,
124) -> Result<PasswordHash, HashError> {
125 let hash = hash_password_inner(opts, password.as_bytes())?;
126
127 Ok(PasswordHash {
128 salt: opts.salt,
129 iterations: opts.iterations,
130 hash,
131 })
132}
133
134pub fn scram256_hash(password: &Password, iterations: &NonZeroU32) -> Result<String, HashError> {
138 let hashed_password = hash_password(password, iterations)?;
139 Ok(scram256_hash_inner(hashed_password)?.to_string())
140}
141
142fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
143 verify_slices_are_equal(a, b).is_ok()
144}
145
146pub fn scram256_verify(password: &Password, hashed_password: &str) -> Result<(), VerifyError> {
148 let opts = scram256_parse_opts(hashed_password)?;
149 let hashed = hash_password_with_opts(&opts, password).map_err(VerifyError::Hash)?;
150 let scram = scram256_hash_inner(hashed).map_err(VerifyError::Hash)?;
151 if constant_time_compare(hashed_password.as_bytes(), scram.to_string().as_bytes()) {
152 Ok(())
153 } else {
154 Err(VerifyError::InvalidPassword)
155 }
156}
157
158pub fn sasl_verify(
159 hashed_password: &str,
160 proof: &str,
161 auth_message: &str,
162) -> Result<String, VerifyError> {
163 let parts: Vec<&str> = hashed_password.split('$').collect();
165 if parts.len() != 3 {
166 return Err(VerifyError::MalformedHash);
167 }
168 let auth_info = parts[1].split(':').collect::<Vec<&str>>();
169 if auth_info.len() != 2 {
170 return Err(VerifyError::MalformedHash);
171 }
172 let auth_value = parts[2].split(':').collect::<Vec<&str>>();
173 if auth_value.len() != 2 {
174 return Err(VerifyError::MalformedHash);
175 }
176
177 let stored_key = Zeroizing::new(
178 BASE64_STANDARD
179 .decode(auth_value[0])
180 .map_err(|_| VerifyError::MalformedHash)?,
181 );
182 let server_key = Zeroizing::new(
183 BASE64_STANDARD
184 .decode(auth_value[1])
185 .map_err(|_| VerifyError::MalformedHash)?,
186 );
187
188 let client_signature = Zeroizing::new(generate_signature(&stored_key, auth_message)?);
190
191 let provided_client_proof = Zeroizing::new(
193 BASE64_STANDARD
194 .decode(proof)
195 .map_err(|_| VerifyError::InvalidPassword)?,
196 );
197
198 if provided_client_proof.len() != client_signature.len() {
199 return Err(VerifyError::InvalidPassword);
200 }
201
202 let client_key: Zeroizing<Vec<u8>> = Zeroizing::new(
204 provided_client_proof
205 .iter()
206 .zip_eq(client_signature.iter())
207 .map(|(p, s)| p ^ s)
208 .collect(),
209 );
210
211 let computed_stored_key = digest::digest(&digest::SHA256, &client_key);
212 if !constant_time_compare(computed_stored_key.as_ref(), &stored_key) {
213 return Err(VerifyError::InvalidPassword);
214 }
215
216 let verifier = Zeroizing::new(generate_signature(&server_key, auth_message)?);
218 Ok(BASE64_STANDARD.encode(&*verifier))
219}
220
221fn generate_signature(key: &[u8], message: &str) -> Result<Zeroizing<Vec<u8>>, VerifyError> {
222 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, key);
223 let tag = hmac::sign(&signing_key, message.as_bytes());
224 Ok(Zeroizing::new(tag.as_ref().to_vec()))
225}
226
227pub fn mock_sasl_challenge(username: &str, mock_nonce: &str, iterations: &NonZeroU32) -> HashOpts {
231 let mut buf = Vec::with_capacity(username.len() + mock_nonce.len());
232 buf.extend_from_slice(username.as_bytes());
233 buf.extend_from_slice(mock_nonce.as_bytes());
234 let hash = digest::digest(&digest::SHA256, &buf);
235 let mut salt = [0u8; DEFAULT_SALT_SIZE];
236 salt.copy_from_slice(hash.as_ref());
237
238 HashOpts {
239 iterations: iterations.to_owned(),
240 salt,
241 }
242}
243
244pub fn scram256_parse_opts(hashed_password: &str) -> Result<HashOpts, VerifyError> {
246 let parts: Vec<&str> = hashed_password.split('$').collect();
247 if parts.len() != 3 {
248 return Err(VerifyError::MalformedHash);
249 }
250 let scheme = parts[0];
251 if scheme != "SCRAM-SHA-256" {
252 return Err(VerifyError::MalformedHash);
253 }
254 let auth_info = parts[1].split(':').collect::<Vec<&str>>();
255 if auth_info.len() != 2 {
256 return Err(VerifyError::MalformedHash);
257 }
258 let auth_value = parts[2].split(':').collect::<Vec<&str>>();
259 if auth_value.len() != 2 {
260 return Err(VerifyError::MalformedHash);
261 }
262
263 let iterations = auth_info[0]
264 .parse::<u32>()
265 .map_err(|_| VerifyError::MalformedHash)?;
266
267 let salt = BASE64_STANDARD
268 .decode(auth_info[1])
269 .map_err(|_| VerifyError::MalformedHash)?;
270
271 let salt = salt.try_into().map_err(|_| VerifyError::MalformedHash)?;
272
273 Ok(HashOpts {
274 iterations: NonZeroU32::new(iterations).ok_or(VerifyError::MalformedHash)?,
275 salt,
276 })
277}
278
279struct ScramSha256Hash {
281 iterations: NonZeroU32,
283 salt: [u8; 32],
285 server_key: [u8; SHA256_OUTPUT_LEN],
287 stored_key: [u8; SHA256_OUTPUT_LEN],
289}
290
291impl Drop for ScramSha256Hash {
292 fn drop(&mut self) {
293 self.salt.zeroize();
294 self.server_key.zeroize();
295 self.stored_key.zeroize();
296 }
297}
298
299impl Display for ScramSha256Hash {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 write!(
302 f,
303 "SCRAM-SHA-256${}:{}${}:{}",
304 self.iterations,
305 BASE64_STANDARD.encode(&self.salt),
306 BASE64_STANDARD.encode(&self.stored_key),
307 BASE64_STANDARD.encode(&self.server_key)
308 )
309 }
310}
311
312fn scram256_hash_inner(hashed_password: PasswordHash) -> Result<ScramSha256Hash, HashError> {
313 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &hashed_password.hash);
314
315 let mut client_key = Zeroizing::new([0u8; SHA256_OUTPUT_LEN]);
320 hmac::sign_to_buffer(&signing_key, b"Client Key", &mut *client_key)
321 .map_err(HashError::Crypto)?;
322 let stored_key_digest = digest::digest(&digest::SHA256, &*client_key);
323 let mut stored_key = [0u8; SHA256_OUTPUT_LEN];
324 stored_key.copy_from_slice(stored_key_digest.as_ref());
325
326 let mut server_key = Zeroizing::new([0u8; SHA256_OUTPUT_LEN]);
327 hmac::sign_to_buffer(&signing_key, b"Server Key", &mut *server_key)
328 .map_err(HashError::Crypto)?;
329
330 Ok(ScramSha256Hash {
331 iterations: hashed_password.iterations,
332 salt: hashed_password.salt,
333 server_key: *server_key,
334 stored_key,
335 })
336}
337
338fn hash_password_inner(
339 opts: &HashOpts,
340 password: &[u8],
341) -> Result<[u8; SHA256_OUTPUT_LEN], HashError> {
342 let mut salted_password = Zeroizing::new([0u8; SHA256_OUTPUT_LEN]);
343 aws_lc_rs::pbkdf2::derive(
344 aws_lc_rs::pbkdf2::PBKDF2_HMAC_SHA256,
345 opts.iterations,
346 &opts.salt,
347 password,
348 &mut *salted_password,
349 );
350 Ok(*salted_password)
351}
352
353#[cfg(test)]
354mod tests {
355 use itertools::Itertools;
356
357 use super::*;
358
359 const DEFAULT_ITERATIONS: NonZeroU32 = NonZeroU32::new(60).expect("Trust me on this");
360
361 #[mz_ore::test]
362 #[cfg_attr(miri, ignore)] fn test_hash_password() {
364 let password = "password".to_string();
365 let iterations = NonZeroU32::new(100).expect("Trust me on this");
366 let hashed_password =
367 hash_password(&password.into(), &iterations).expect("Failed to hash password");
368 assert_eq!(hashed_password.iterations, iterations);
369 assert_eq!(hashed_password.salt.len(), DEFAULT_SALT_SIZE);
370 assert_eq!(hashed_password.hash.len(), SHA256_OUTPUT_LEN);
371 }
372
373 #[mz_ore::test]
374 #[cfg_attr(miri, ignore)] fn test_scram256_hash() {
376 let password = "password".into();
377 let scram_hash =
378 scram256_hash(&password, &DEFAULT_ITERATIONS).expect("Failed to hash password");
379
380 let res = scram256_verify(&password, &scram_hash);
381 assert!(res.is_ok());
382 let res = scram256_verify(&"wrong_password".into(), &scram_hash);
383 assert!(res.is_err());
384 }
385
386 #[mz_ore::test]
387 fn test_scram256_parse_opts() {
388 let salt = "9bkIQQjQ7f1OwPsXZGC/YfIkbZsOMDXK0cxxvPBaSfM=";
389 let hashed_password = format!("SCRAM-SHA-256$600000:{}$client-key:server-key", salt);
390 let opts = scram256_parse_opts(&hashed_password);
391
392 assert!(opts.is_ok());
393 let opts = opts.unwrap();
394 assert_eq!(
395 opts.iterations,
396 NonZeroU32::new(600_000).expect("known valid")
397 );
398 assert_eq!(opts.salt.len(), DEFAULT_SALT_SIZE);
399 let decoded_salt = BASE64_STANDARD.decode(salt).expect("Failed to decode salt");
400 assert_eq!(opts.salt, decoded_salt.as_ref());
401 }
402
403 #[mz_ore::test]
404 #[cfg_attr(miri, ignore)]
405 fn test_mock_sasl_challenge() {
406 let username = "alice";
407 let mock = "cnonce";
408 let opts1 = mock_sasl_challenge(username, mock, &DEFAULT_ITERATIONS);
409 let opts2 = mock_sasl_challenge(username, mock, &DEFAULT_ITERATIONS);
410 assert_eq!(opts1, opts2);
411 }
412
413 #[mz_ore::test]
414 #[cfg_attr(miri, ignore)]
415 fn test_sasl_verify_success() {
416 let password: Password = "password".into();
417 let hashed_password = scram256_hash(&password, &DEFAULT_ITERATIONS).expect("hash password");
418 let auth_message = "n=user,r=clientnonce,s=somesalt"; let parts: Vec<&str> = hashed_password.split('$').collect();
423 assert_eq!(parts.len(), 3);
424 let key_parts: Vec<&str> = parts[2].split(':').collect();
425 assert_eq!(key_parts.len(), 2);
426 let stored_key = BASE64_STANDARD
427 .decode(key_parts[0])
428 .expect("decode stored key");
429 let server_key = BASE64_STANDARD
430 .decode(key_parts[1])
431 .expect("decode server key");
432
433 let client_proof: Vec<u8> = {
435 let opts = scram256_parse_opts(&hashed_password).expect("parse opts");
437 let salted_password = hash_password_with_opts(&opts, &password)
438 .expect("hash password")
439 .hash;
440 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &salted_password);
441 let client_key = hmac::sign(&signing_key, b"Client Key");
442 let client_key = client_key.as_ref();
443 let client_signature =
445 generate_signature(&stored_key, auth_message).expect("client signature");
446 client_key
447 .iter()
448 .zip_eq(client_signature.iter())
449 .map(|(c, s)| c ^ s)
450 .collect::<Vec<u8>>()
451 };
452
453 let client_proof_b64 = BASE64_STANDARD.encode(&client_proof);
454
455 let verifier = sasl_verify(&hashed_password, &client_proof_b64, auth_message)
456 .expect("sasl_verify should succeed");
457
458 let expected_verifier = BASE64_STANDARD
460 .encode(&generate_signature(&server_key, auth_message).expect("server verifier"));
461 assert_eq!(verifier, expected_verifier);
462 }
463
464 #[mz_ore::test]
465 #[cfg_attr(miri, ignore)]
466 fn test_sasl_verify_invalid_proof() {
467 let password: Password = "password".into();
468 let hashed_password = scram256_hash(&password, &DEFAULT_ITERATIONS).expect("hash password");
469 let auth_message = "n=user,r=clientnonce,s=somesalt";
470 let bad_proof = BASE64_STANDARD.encode([0u8; 32]);
472 let res = sasl_verify(&hashed_password, &bad_proof, auth_message);
473 assert!(matches!(res, Err(VerifyError::InvalidPassword)));
474 }
475
476 #[mz_ore::test]
477 fn test_sasl_verify_malformed_hash() {
478 let malformed_hash = "NOT-SCRAM$bad"; let auth_message = "n=user,r=clientnonce,s=somesalt";
480 let bad_proof = BASE64_STANDARD.encode([0u8; 32]);
481 let res = sasl_verify(malformed_hash, &bad_proof, auth_message);
482 assert!(matches!(res, Err(VerifyError::MalformedHash)));
483 }
484
485 #[mz_ore::test]
486 #[cfg_attr(miri, ignore)]
487 fn test_sasl_verify_truncated_proof_no_panic() {
488 let password: Password = "password".into();
490 let hashed_password = scram256_hash(&password, &DEFAULT_ITERATIONS).expect("hash password");
491 let auth_message = "n=user,r=clientnonce,s=somesalt";
492
493 let truncated_proof = BASE64_STANDARD.encode([0u8; 16]);
495 let res = sasl_verify(&hashed_password, &truncated_proof, auth_message);
496 assert!(
497 matches!(res, Err(VerifyError::InvalidPassword)),
498 "truncated proof should return InvalidPassword, not panic"
499 );
500
501 let oversized_proof = BASE64_STANDARD.encode([0u8; 64]);
503 let res = sasl_verify(&hashed_password, &oversized_proof, auth_message);
504 assert!(
505 matches!(res, Err(VerifyError::InvalidPassword)),
506 "oversized proof should return InvalidPassword, not panic"
507 );
508
509 let empty_proof = BASE64_STANDARD.encode([0u8; 0]);
511 let res = sasl_verify(&hashed_password, &empty_proof, auth_message);
512 assert!(
513 matches!(res, Err(VerifyError::InvalidPassword)),
514 "empty proof should return InvalidPassword, not panic"
515 );
516 }
517}