1use std::{fmt, str::FromStr};
4
5use base64::{Engine, engine::general_purpose::STANDARD as B64};
6use thiserror::Error;
7
8use super::{ScramCredential, hash_scram_password, hash_scram_password_with_salt};
9use crate::SaslMechanism;
10
11const POSTGRES_SCRAM_SHA256_PREFIX: &str = "SCRAM-SHA-256";
12const SHA256_KEY_LEN: usize = 32;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PgScramVerifier {
20 pub iterations: u32,
22 pub salt: Vec<u8>,
24 pub stored_key: Vec<u8>,
26 pub server_key: Vec<u8>,
28}
29
30#[derive(Debug, Error, Clone, PartialEq, Eq)]
32pub enum ScramError {
33 #[error("malformed PostgreSQL SCRAM verifier")]
35 MalformedVerifier,
36 #[error("unsupported SCRAM verifier mechanism")]
38 UnsupportedMechanism,
39 #[error("SCRAM iteration count must be greater than zero")]
41 InvalidIterations,
42 #[error("SCRAM salt must not be empty")]
44 EmptySalt,
45 #[error("invalid base64 in SCRAM verifier")]
47 InvalidBase64,
48 #[error("invalid {field} length: expected {expected} bytes, got {actual}")]
50 InvalidKeyLength {
51 field: &'static str,
53 expected: usize,
55 actual: usize,
57 },
58 #[error("system random number generator unavailable")]
60 RandomUnavailable,
61}
62
63impl PgScramVerifier {
64 pub fn generate(password: &str, iterations: u32) -> Result<Self, ScramError> {
72 if iterations == 0 {
73 return Err(ScramError::InvalidIterations);
74 }
75
76 let credential =
77 hash_scram_password(password.as_bytes(), SaslMechanism::ScramSha256, iterations)?;
78 Ok(Self::from_sha256_credential(credential))
79 }
80
81 pub fn generate_with_salt(
87 password: &str,
88 iterations: u32,
89 salt: Vec<u8>,
90 ) -> Result<Self, ScramError> {
91 if iterations == 0 {
92 return Err(ScramError::InvalidIterations);
93 }
94 if salt.is_empty() {
95 return Err(ScramError::EmptySalt);
96 }
97
98 let credential = hash_scram_password_with_salt(
99 password.as_bytes(),
100 SaslMechanism::ScramSha256,
101 iterations,
102 salt,
103 );
104 Ok(Self::from_sha256_credential(credential))
105 }
106
107 pub fn parse(verifier: &str) -> Result<Self, ScramError> {
114 verifier.parse()
115 }
116
117 fn parse_fields(verifier: &str) -> Result<Self, ScramError> {
118 let (prefix, rest) = verifier
119 .split_once('$')
120 .ok_or(ScramError::MalformedVerifier)?;
121 if prefix != POSTGRES_SCRAM_SHA256_PREFIX {
122 return Err(ScramError::UnsupportedMechanism);
123 }
124
125 let (iterations_and_salt, keys) =
126 rest.split_once('$').ok_or(ScramError::MalformedVerifier)?;
127 if keys.contains('$') {
128 return Err(ScramError::MalformedVerifier);
129 }
130
131 let (iterations, salt) = parse_iterations_and_salt(iterations_and_salt)?;
132 let (stored_key, server_key) = parse_key_pair(keys)?;
133
134 Ok(Self {
135 iterations,
136 salt,
137 stored_key,
138 server_key,
139 })
140 }
141
142 fn from_sha256_credential(credential: ScramCredential) -> Self {
143 debug_assert_eq!(credential.mechanism, SaslMechanism::ScramSha256);
144 Self {
145 iterations: credential.iterations,
146 salt: credential.salt,
147 stored_key: credential.stored_key,
148 server_key: credential.server_key,
149 }
150 }
151}
152
153impl fmt::Display for PgScramVerifier {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(
156 f,
157 "{POSTGRES_SCRAM_SHA256_PREFIX}${}:{}${}:{}",
158 self.iterations,
159 B64.encode(&self.salt),
160 B64.encode(&self.stored_key),
161 B64.encode(&self.server_key)
162 )
163 }
164}
165
166impl FromStr for PgScramVerifier {
167 type Err = ScramError;
168
169 fn from_str(verifier: &str) -> Result<Self, Self::Err> {
170 Self::parse_fields(verifier)
171 }
172}
173
174impl From<PgScramVerifier> for ScramCredential {
175 fn from(verifier: PgScramVerifier) -> Self {
176 Self {
177 mechanism: SaslMechanism::ScramSha256,
178 salt: verifier.salt,
179 stored_key: verifier.stored_key,
180 server_key: verifier.server_key,
181 iterations: verifier.iterations,
182 }
183 }
184}
185
186impl From<&PgScramVerifier> for ScramCredential {
187 fn from(verifier: &PgScramVerifier) -> Self {
188 Self {
189 mechanism: SaslMechanism::ScramSha256,
190 salt: verifier.salt.clone(),
191 stored_key: verifier.stored_key.clone(),
192 server_key: verifier.server_key.clone(),
193 iterations: verifier.iterations,
194 }
195 }
196}
197
198fn parse_iterations_and_salt(input: &str) -> Result<(u32, Vec<u8>), ScramError> {
199 let (iterations, salt_b64) = input.split_once(':').ok_or(ScramError::MalformedVerifier)?;
200 if salt_b64.contains(':') {
201 return Err(ScramError::MalformedVerifier);
202 }
203
204 let iterations = iterations
205 .parse::<u32>()
206 .map_err(|_| ScramError::MalformedVerifier)?;
207 if iterations == 0 {
208 return Err(ScramError::InvalidIterations);
209 }
210
211 let salt = B64
212 .decode(salt_b64)
213 .map_err(|_| ScramError::InvalidBase64)?;
214 if salt.is_empty() {
215 return Err(ScramError::EmptySalt);
216 }
217
218 Ok((iterations, salt))
219}
220
221fn parse_key_pair(input: &str) -> Result<(Vec<u8>, Vec<u8>), ScramError> {
222 let (stored_key_b64, server_key_b64) =
223 input.split_once(':').ok_or(ScramError::MalformedVerifier)?;
224 if server_key_b64.contains(':') {
225 return Err(ScramError::MalformedVerifier);
226 }
227
228 let stored_key = B64
229 .decode(stored_key_b64)
230 .map_err(|_| ScramError::InvalidBase64)?;
231 let server_key = B64
232 .decode(server_key_b64)
233 .map_err(|_| ScramError::InvalidBase64)?;
234 require_key_len("stored_key", &stored_key)?;
235 require_key_len("server_key", &server_key)?;
236
237 Ok((stored_key, server_key))
238}
239
240fn require_key_len(field: &'static str, key: &[u8]) -> Result<(), ScramError> {
241 if key.len() == SHA256_KEY_LEN {
242 return Ok(());
243 }
244
245 Err(ScramError::InvalidKeyLength {
246 field,
247 expected: SHA256_KEY_LEN,
248 actual: key.len(),
249 })
250}
251
252#[cfg(test)]
253mod tests {
254 use std::process;
255
256 use assert2::{assert, check};
257
258 use super::*;
259 use crate::scram::{ScramClientExchange, ScramServerExchange, StepResult};
260
261 const FIXED_SALT: &[u8] = b"0123456789abcdef";
262
263 fn fixture_password() -> String {
264 process::id().to_string()
265 }
266
267 fn pg_scram_golden_password() -> &'static str {
273 include_str!("../../tests/fixtures/pg_scram_password.txt").trim_end()
274 }
275
276 #[test]
277 fn parse_display_roundtrip_emits_canonical_verifier() {
278 let verifier =
279 PgScramVerifier::generate_with_salt(&fixture_password(), 4096, FIXED_SALT.to_vec())
280 .expect("fixed salt verifier");
281
282 let rendered = verifier.to_string();
283 let parsed = PgScramVerifier::parse(&rendered).expect("rendered verifier parses");
284
285 assert!(parsed == verifier);
286 assert!(parsed.to_string() == rendered);
287 }
288
289 #[test]
290 fn deterministic_generation_with_fixed_salt_matches_known_sha256_material() {
291 let verifier = PgScramVerifier::generate_with_salt(
292 pg_scram_golden_password(),
293 4096,
294 (0_u8..16).collect::<Vec<_>>(),
295 )
296 .expect("fixed salt verifier");
297
298 check!(verifier.iterations == 4096);
299 check!(verifier.salt == (0_u8..16).collect::<Vec<_>>());
300 check!(B64.encode(&verifier.stored_key) == "ijmdkBF6VaUuRpEzxe8P8NyZKzuHGkoluJBVZ5DF5+Q=");
301 check!(B64.encode(&verifier.server_key) == "naMMD2q/yKfDcTgfNhpZqsbkzoRQwOPfT/TgHEPWP7w=");
302 assert!(
303 verifier.to_string()
304 == "SCRAM-SHA-256$4096:AAECAwQFBgcICQoLDA0ODw==$ijmdkBF6VaUuRpEzxe8P8NyZKzuHGkoluJBVZ5DF5+Q=:naMMD2q/yKfDcTgfNhpZqsbkzoRQwOPfT/TgHEPWP7w="
305 );
306 }
307
308 #[test]
309 fn rejects_malformed_verifier_table() {
310 let wrong_stored_key = B64.encode([0_u8; SHA256_KEY_LEN - 1]);
311 let wrong_server_key = B64.encode([0_u8; SHA256_KEY_LEN + 1]);
312 let valid_server_key = B64.encode([1_u8; SHA256_KEY_LEN]);
313
314 for (input, expected) in [
315 ("", ScramError::MalformedVerifier),
316 (
317 "SCRAM-SHA-512$4096:c2FsdA==$a:b",
318 ScramError::UnsupportedMechanism,
319 ),
320 ("md5deadbeef", ScramError::MalformedVerifier),
321 ("SCRAM-SHA-256", ScramError::MalformedVerifier),
322 ("SCRAM-SHA-256$4096:c2FsdA==", ScramError::MalformedVerifier),
323 (
324 "SCRAM-SHA-256$4096:c2FsdA==$AAAA:AAAA$extra",
325 ScramError::MalformedVerifier,
326 ),
327 (
328 "SCRAM-SHA-256$4096:c2FsdA==:extra$AAAA:AAAA",
329 ScramError::MalformedVerifier,
330 ),
331 (
332 "SCRAM-SHA-256$4096:c2FsdA==$AAAA:AAAA:extra",
333 ScramError::MalformedVerifier,
334 ),
335 (
336 "SCRAM-SHA-256$not-a-number:c2FsdA==$AAAA:AAAA",
337 ScramError::MalformedVerifier,
338 ),
339 (
340 "SCRAM-SHA-256$0:c2FsdA==$AAAA:AAAA",
341 ScramError::InvalidIterations,
342 ),
343 ("SCRAM-SHA-256$4096:*$AAAA:AAAA", ScramError::InvalidBase64),
344 ("SCRAM-SHA-256$4096:$AAAA:AAAA", ScramError::EmptySalt),
345 (
346 "SCRAM-SHA-256$4096:c2FsdA==$*:AAAA",
347 ScramError::InvalidBase64,
348 ),
349 (
350 "SCRAM-SHA-256$4096:c2FsdA==$AAAA:*",
351 ScramError::InvalidBase64,
352 ),
353 (
354 &format!("SCRAM-SHA-256$4096:c2FsdA==${wrong_stored_key}:{valid_server_key}"),
355 ScramError::InvalidKeyLength {
356 field: "stored_key",
357 expected: SHA256_KEY_LEN,
358 actual: SHA256_KEY_LEN - 1,
359 },
360 ),
361 (
362 &format!("SCRAM-SHA-256$4096:c2FsdA==${valid_server_key}:{wrong_server_key}"),
363 ScramError::InvalidKeyLength {
364 field: "server_key",
365 expected: SHA256_KEY_LEN,
366 actual: SHA256_KEY_LEN + 1,
367 },
368 ),
369 ] {
370 assert!(
371 PgScramVerifier::parse(input) == Err(expected),
372 "case {input}"
373 );
374 }
375 }
376
377 #[test]
378 fn parses_postgresql_format_fixture() {
379 let fixture = "SCRAM-SHA-256$4096:AAECAwQFBgcICQoLDA0ODw==$ijmdkBF6VaUuRpEzxe8P8NyZKzuHGkoluJBVZ5DF5+Q=:naMMD2q/yKfDcTgfNhpZqsbkzoRQwOPfT/TgHEPWP7w=";
382
383 let verifier = PgScramVerifier::parse(fixture).expect("PostgreSQL verifier parses");
384
385 check!(verifier.iterations == 4096);
386 check!(verifier.salt == (0_u8..16).collect::<Vec<_>>());
387 check!(verifier.stored_key.len() == SHA256_KEY_LEN);
388 check!(verifier.server_key.len() == SHA256_KEY_LEN);
389 assert!(verifier.to_string() == fixture);
390 }
391
392 #[test]
393 fn generated_verifier_material_authenticates_scram_sha256_exchange() {
394 let password = fixture_password();
395 let verifier = PgScramVerifier::generate(&password, 4096).expect("fresh verifier");
396 let credential = ScramCredential::from(&verifier);
397 let server = ScramServerExchange::new("alice".to_owned(), credential);
398 let client = ScramClientExchange::new(
399 "alice".to_owned(),
400 password.into_bytes(),
401 SaslMechanism::ScramSha256,
402 );
403
404 let (client_first, client) = client.client_first().expect("client first");
405 let (server_first, server) = match server.step(&client_first) {
406 StepResult::Continue(bytes, exchange) => (bytes, exchange),
407 other => panic!("server first must continue, got {other:?}"),
408 };
409 let (client_final, client) = client.step(&server_first).expect("client final");
410 let (principal, server_final) = match server.step(&client_final) {
411 StepResult::Done(principal, bytes) => (principal, bytes),
412 other => panic!("server final must finish, got {other:?}"),
413 };
414
415 check!(principal.name == "alice");
416 check!(principal.auth_method == crate::AuthMethod::SaslScramSha256);
417 assert!(client.verify_server_final(&server_final).is_ok());
418 }
419}