Line data Source code
1 : //! Tools for SCRAM server secret management.
2 :
3 : use base64::Engine as _;
4 : use base64::prelude::BASE64_STANDARD;
5 : use subtle::{Choice, ConstantTimeEq};
6 :
7 : use super::base64_decode_array;
8 : use super::key::ScramKey;
9 :
10 : /// Server secret is produced from user's password,
11 : /// and is used throughout the authentication process.
12 : #[derive(Clone, Eq, PartialEq, Debug)]
13 : pub(crate) struct ServerSecret {
14 : /// Number of iterations for `PBKDF2` function.
15 : pub(crate) iterations: u32,
16 : /// Salt used to hash user's password.
17 : pub(crate) salt_base64: Box<str>,
18 : /// Hashed `ClientKey`.
19 : pub(crate) stored_key: ScramKey,
20 : /// Used by client to verify server's signature.
21 : pub(crate) server_key: ScramKey,
22 : /// Should auth fail no matter what?
23 : /// This is exactly the case for mocked secrets.
24 : pub(crate) doomed: bool,
25 : }
26 :
27 : impl ServerSecret {
28 18 : pub(crate) fn parse(input: &str) -> Option<Self> {
29 : // SCRAM-SHA-256$<iterations>:<salt>$<storedkey>:<serverkey>
30 18 : let s = input.strip_prefix("SCRAM-SHA-256$")?;
31 18 : let (params, keys) = s.split_once('$')?;
32 :
33 18 : let ((iterations, salt), (stored_key, server_key)) =
34 18 : params.split_once(':').zip(keys.split_once(':'))?;
35 :
36 18 : let secret = ServerSecret {
37 18 : iterations: iterations.parse().ok()?,
38 18 : salt_base64: salt.into(),
39 18 : stored_key: base64_decode_array(stored_key)?.into(),
40 18 : server_key: base64_decode_array(server_key)?.into(),
41 : doomed: false,
42 : };
43 :
44 18 : Some(secret)
45 18 : }
46 :
47 12 : pub(crate) fn is_password_invalid(&self, client_key: &ScramKey) -> Choice {
48 : // constant time to not leak partial key match
49 12 : client_key.sha256().ct_ne(&self.stored_key) | Choice::from(self.doomed as u8)
50 12 : }
51 :
52 : /// To avoid revealing information to an attacker, we use a
53 : /// mocked server secret even if the user doesn't exist.
54 : /// See `auth-scram.c : mock_scram_secret` for details.
55 3 : pub(crate) fn mock(nonce: [u8; 32]) -> Self {
56 3 : Self {
57 3 : // this doesn't reveal much information as we're going to use
58 3 : // iteration count 1 for our generated passwords going forward.
59 3 : // PG16 users can set iteration count=1 already today.
60 3 : iterations: 1,
61 3 : salt_base64: BASE64_STANDARD.encode(nonce).into_boxed_str(),
62 3 : stored_key: ScramKey::default(),
63 3 : server_key: ScramKey::default(),
64 3 : doomed: true,
65 3 : }
66 3 : }
67 :
68 : /// Build a new server secret from the prerequisites.
69 : /// XXX: We only use this function in tests.
70 : #[cfg(test)]
71 16 : pub(crate) async fn build(password: &str) -> Option<Self> {
72 16 : Self::parse(&postgres_protocol::password::scram_sha_256(password.as_bytes()).await)
73 16 : }
74 : }
75 :
76 : #[cfg(test)]
77 : mod tests {
78 : use super::*;
79 :
80 : #[test]
81 1 : fn parse_scram_secret() {
82 1 : let iterations = 4096;
83 1 : let salt = "+/tQQax7twvwTj64mjBsxQ==";
84 1 : let stored_key = "D5h6KTMBlUvDJk2Y8ELfC1Sjtc6k9YHjRyuRZyBNJns=";
85 1 : let server_key = "Pi3QHbcluX//NDfVkKlFl88GGzlJ5LkyPwcdlN/QBvI=";
86 :
87 1 : let secret = format!("SCRAM-SHA-256${iterations}:{salt}${stored_key}:{server_key}");
88 :
89 1 : let parsed = ServerSecret::parse(&secret).unwrap();
90 1 : assert_eq!(parsed.iterations, iterations);
91 1 : assert_eq!(&*parsed.salt_base64, salt);
92 :
93 1 : assert_eq!(BASE64_STANDARD.encode(parsed.stored_key), stored_key);
94 1 : assert_eq!(BASE64_STANDARD.encode(parsed.server_key), server_key);
95 1 : }
96 : }
|