Skip to main content

reddb_server/auth/
scram.rs

1//! SCRAM-SHA-256 (RFC 5802 + RFC 7677) primitives.
2//!
3//! Pure functions — no I/O, no state. Both server and client use
4//! the same key-derivation routines; the state machine lives in
5//! `wire::redwire::auth` for the server and the driver crates
6//! for clients. Layout choices match what PostgreSQL ≥10 ships
7//! so RedDB peers with libpq-style tooling for free.
8//!
9//! Verifier storage:
10//!   { salt, iter, stored_key, server_key }
11//!
12//! `salted_password = PBKDF2-HMAC-SHA256(password, salt, iter, 32)`
13//! `client_key       = HMAC-SHA256(salted_password, "Client Key")`
14//! `stored_key       = SHA256(client_key)`
15//! `server_key       = HMAC-SHA256(salted_password, "Server Key")`
16//!
17//! On login, the server never sees plaintext — only proof / signature.
18
19use sha2::{Digest, Sha256};
20
21use crate::storage::encryption::pbkdf2::pbkdf2_sha256;
22use crate::storage::encryption::pbkdf2::Pbkdf2Params;
23
24/// Default iteration count. RFC 7677 recommends 4096; we go higher
25/// because RedDB targets 2025+ hardware. Operators can override
26/// per-user when migrating from a different database.
27pub const DEFAULT_ITER: u32 = 16_384;
28
29/// Minimum iteration count we'll accept on a stored verifier.
30/// Below this we treat the verifier as unsafe and force a rotation.
31pub const MIN_ITER: u32 = 4096;
32
33/// Stored verifier — what the server keeps in `AuthStore` per
34/// SCRAM-enabled user. Never contains plaintext or
35/// `salted_password`.
36#[derive(Debug, Clone)]
37pub struct ScramVerifier {
38    pub salt: Vec<u8>,
39    pub iter: u32,
40    pub stored_key: [u8; 32],
41    pub server_key: [u8; 32],
42}
43
44impl ScramVerifier {
45    /// Derive a verifier from a plaintext password. Used once at
46    /// account creation / password rotation.
47    pub fn from_password(password: &str, salt: Vec<u8>, iter: u32) -> Self {
48        let salted = salted_password(password.as_bytes(), &salt, iter);
49        let client_key = hmac_sha256(&salted, b"Client Key");
50        let stored_key: [u8; 32] = sha256(&client_key);
51        let server_key = hmac_sha256(&salted, b"Server Key");
52        Self {
53            salt,
54            iter,
55            stored_key,
56            server_key,
57        }
58    }
59}
60
61/// Compute `SaltedPassword`. RFC 5802 § 3 — PBKDF2 with HMAC-SHA256.
62pub fn salted_password(password: &[u8], salt: &[u8], iter: u32) -> [u8; 32] {
63    let params = Pbkdf2Params {
64        iterations: iter,
65        // pbkdf2_sha256 uses a fixed 32-byte derived length.
66        ..Pbkdf2Params::default()
67    };
68    let v = pbkdf2_sha256(password, salt, &params);
69    let mut out = [0u8; 32];
70    out.copy_from_slice(&v[..32]);
71    out
72}
73
74/// HMAC-SHA256(key, data) → 32 bytes. Reuses the engine's
75/// internal helper at `crate::crypto::hmac_sha256`.
76pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
77    crate::crypto::hmac_sha256(key, data)
78}
79
80/// SHA-256(data) → 32 bytes.
81pub fn sha256(data: &[u8]) -> [u8; 32] {
82    let mut hasher = Sha256::new();
83    hasher.update(data);
84    let mut out = [0u8; 32];
85    out.copy_from_slice(&hasher.finalize());
86    out
87}
88
89/// XOR two equal-length byte slices into a fresh `Vec<u8>`.
90/// Used for `ClientProof = ClientKey XOR ClientSignature`.
91pub fn xor(a: &[u8], b: &[u8]) -> Vec<u8> {
92    a.iter().zip(b.iter()).map(|(x, y)| x ^ y).collect()
93}
94
95/// Build the canonical `AuthMessage` per RFC 5802 § 3:
96///     client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
97pub fn auth_message(
98    client_first_bare: &str,
99    server_first: &str,
100    client_final_no_proof: &str,
101) -> Vec<u8> {
102    let mut out = Vec::with_capacity(
103        client_first_bare.len() + 1 + server_first.len() + 1 + client_final_no_proof.len(),
104    );
105    out.extend_from_slice(client_first_bare.as_bytes());
106    out.push(b',');
107    out.extend_from_slice(server_first.as_bytes());
108    out.push(b',');
109    out.extend_from_slice(client_final_no_proof.as_bytes());
110    out
111}
112
113/// Compute the client's proof — what the client sends to prove
114/// it knows the password.
115pub fn client_proof(stored_key: &[u8], auth_message: &[u8], client_key: &[u8]) -> Vec<u8> {
116    let signature = hmac_sha256(stored_key, auth_message);
117    xor(client_key, &signature)
118}
119
120/// Verify a client proof against a stored verifier. Returns true
121/// when the proof matches.
122pub fn verify_client_proof(
123    verifier: &ScramVerifier,
124    auth_message: &[u8],
125    presented_proof: &[u8],
126) -> bool {
127    if presented_proof.len() != 32 {
128        return false;
129    }
130    // ClientKey = ClientProof XOR ClientSignature
131    let signature = hmac_sha256(&verifier.stored_key, auth_message);
132    let client_key = xor(presented_proof, &signature);
133    let derived_stored: [u8; 32] = sha256(&client_key);
134    crate::crypto::constant_time_eq(&derived_stored, &verifier.stored_key)
135}
136
137/// Server's signature to send back in `AuthOk` — proves to the
138/// client that the server also knows the verifier.
139pub fn server_signature(server_key: &[u8], auth_message: &[u8]) -> [u8; 32] {
140    hmac_sha256(server_key, auth_message)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    /// Sanity: deriving the same verifier twice from the same
148    /// inputs yields identical keys.
149    #[test]
150    fn verifier_is_deterministic() {
151        let salt = b"reddb-test-salt".to_vec();
152        let v1 = ScramVerifier::from_password("hunter2", salt.clone(), 4096);
153        let v2 = ScramVerifier::from_password("hunter2", salt, 4096);
154        assert_eq!(v1.stored_key, v2.stored_key);
155        assert_eq!(v1.server_key, v2.server_key);
156    }
157
158    /// End-to-end: derive verifier, simulate client computing the
159    /// proof, server verifies. Round-trip succeeds for the right
160    /// password and fails for the wrong one.
161    #[test]
162    fn full_round_trip() {
163        let salt = b"reddb-rt-salt".to_vec();
164        let iter = 4096;
165        let verifier = ScramVerifier::from_password("correct horse", salt.clone(), iter);
166
167        let client_first_bare = "n=alice,r=cnonce";
168        let server_first = "r=cnonce+snonce,s=cmVkZGItcnQtc2FsdA==,i=4096";
169        let client_final_no_proof = "c=biws,r=cnonce+snonce";
170        let am = auth_message(client_first_bare, server_first, client_final_no_proof);
171
172        // Client side computes proof from plaintext.
173        let salted = salted_password(b"correct horse", &salt, iter);
174        let client_key = hmac_sha256(&salted, b"Client Key");
175        let proof = client_proof(&verifier.stored_key, &am, &client_key);
176
177        // Server verifies.
178        assert!(verify_client_proof(&verifier, &am, &proof));
179
180        // Wrong password → wrong client_key → rejected.
181        let salted_bad = salted_password(b"wrong password", &salt, iter);
182        let client_key_bad = hmac_sha256(&salted_bad, b"Client Key");
183        let proof_bad = client_proof(&verifier.stored_key, &am, &client_key_bad);
184        assert!(!verify_client_proof(&verifier, &am, &proof_bad));
185    }
186
187    #[test]
188    fn server_signature_round_trip() {
189        let v = ScramVerifier::from_password("p", b"s".to_vec(), 4096);
190        let am = b"some auth message".to_vec();
191        let sig = server_signature(&v.server_key, &am);
192        // Same inputs → same signature.
193        let again = server_signature(&v.server_key, &am);
194        assert_eq!(sig, again);
195        // Different message → different signature.
196        let other = server_signature(&v.server_key, b"different");
197        assert_ne!(sig, other);
198    }
199
200    #[test]
201    fn xor_basic() {
202        assert_eq!(
203            xor(&[0xff, 0x00, 0xaa], &[0x0f, 0xff, 0x55]),
204            vec![0xf0, 0xff, 0xff]
205        );
206    }
207}