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