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