LCOV - code coverage report
Current view: top level - proxy/src/scram - secret.rs (source / functions) Coverage Total Hit
Test: 1e20c4f2b28aa592527961bb32170ebbd2c9172f.info Lines: 100.0 % 42 42
Test Date: 2025-07-16 12:29: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              : 
       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              : }
        

Generated by: LCOV version 2.1-beta