LCOV - code coverage report
Current view: top level - proxy/src/scram - secret.rs (source / functions) Coverage Total Hit
Test: 1d5975439f3c9882b18414799141ebf9a3922c58.info Lines: 100.0 % 44 44
Test Date: 2025-07-31 15:59:03 Functions: 100.0 % 6 6

            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              : }
        

Generated by: LCOV version 2.1-beta