1use std::{io, str};
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use bytes::Bytes;
7use hmac::{Hmac, KeyInit as _, Mac as _};
8use rand::RngExt as _;
9use sha2::{Digest as _, Sha256};
10use subtle::ConstantTimeEq as _;
11
12pub const SCRAM_SHA_256: &[u8] = b"SCRAM-SHA-256";
14pub const SCRAM_SHA_256_PLUS: &[u8] = b"SCRAM-SHA-256-PLUS";
16pub const DEFAULT_ITERATIONS: u32 = 4096;
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum ServerChannelBinding {
22 None,
24 TlsServerEndPoint(Vec<u8>),
26}
27
28pub struct ScramServer {
30 password: Vec<u8>,
31 salt: Vec<u8>,
32 iterations: u32,
33 channel_binding: ServerChannelBinding,
34}
35
36pub struct ScramExchange {
38 salted_password: [u8; 32],
39 client_first_bare: String,
40 server_first: String,
41 combined_nonce: String,
42 expected_channel_binding: Vec<u8>,
43}
44
45impl ScramServer {
46 #[must_use]
48 pub fn new(password: &[u8], channel_binding: ServerChannelBinding) -> Self {
49 let mut salt = vec![0; 16];
50 rand::rng().fill(&mut salt[..]);
51 Self {
52 password: normalize(password),
53 salt,
54 iterations: DEFAULT_ITERATIONS,
55 channel_binding,
56 }
57 }
58
59 pub fn with_parameters(
65 password: &[u8],
66 salt: Vec<u8>,
67 iterations: u32,
68 channel_binding: ServerChannelBinding,
69 ) -> io::Result<Self> {
70 if iterations < DEFAULT_ITERATIONS {
71 return Err(invalid("SCRAM iteration count is below 4096"));
72 }
73 Ok(Self {
74 password: normalize(password),
75 salt,
76 iterations,
77 channel_binding,
78 })
79 }
80
81 pub fn start(
88 &self,
89 mechanism: &[u8],
90 client_first: &[u8],
91 ) -> io::Result<(ScramExchange, Bytes)> {
92 let client_first = str::from_utf8(client_first)
93 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
94 let (gs2_header, client_first_bare) = split_gs2(client_first)?;
95 let plus = match mechanism {
96 SCRAM_SHA_256 => false,
97 SCRAM_SHA_256_PLUS => true,
98 _ => return Err(invalid("unsupported SCRAM mechanism")),
99 };
100 let expected_channel_binding = self.expected_binding(plus, gs2_header)?;
101 let attributes = attributes(client_first_bare)?;
102 reject_mandatory_extension(&attributes)?;
103 let client_nonce = required(&attributes, b'r')?;
104 validate_nonce(client_nonce)?;
105 let _username = required(&attributes, b'n')?;
106
107 let server_nonce = random_nonce();
108 let combined_nonce = format!("{client_nonce}{server_nonce}");
109 let server_first = format!(
110 "r={combined_nonce},s={},i={}",
111 STANDARD.encode(&self.salt),
112 self.iterations
113 );
114 let exchange = ScramExchange {
115 salted_password: hi(&self.password, &self.salt, self.iterations),
116 client_first_bare: client_first_bare.to_owned(),
117 server_first: server_first.clone(),
118 combined_nonce,
119 expected_channel_binding,
120 };
121 Ok((exchange, Bytes::from(server_first)))
122 }
123
124 fn expected_binding(&self, plus: bool, gs2_header: &str) -> io::Result<Vec<u8>> {
125 match (plus, &self.channel_binding) {
126 (true, ServerChannelBinding::TlsServerEndPoint(binding))
127 if gs2_header == "p=tls-server-end-point,," =>
128 {
129 let mut expected = gs2_header.as_bytes().to_vec();
130 expected.extend_from_slice(binding);
131 Ok(expected)
132 }
133 (true, _) => Err(invalid(
134 "SCRAM-PLUS channel binding is unavailable or invalid",
135 )),
136 (false, ServerChannelBinding::TlsServerEndPoint(_)) if gs2_header == "y,," => {
137 Err(invalid("SCRAM channel-binding downgrade detected"))
138 }
139 (false, _) if matches!(gs2_header, "n,," | "y,,") => Ok(gs2_header.as_bytes().to_vec()),
140 (false, _) => Err(invalid("channel binding used with SCRAM-SHA-256")),
141 }
142 }
143}
144
145impl ScramExchange {
146 pub fn finish(self, client_final: &[u8]) -> io::Result<Bytes> {
153 let client_final = str::from_utf8(client_final)
154 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
155 let proof_marker = client_final
156 .rfind(",p=")
157 .ok_or_else(|| invalid("SCRAM client-final has no proof"))?;
158 let without_proof = &client_final[..proof_marker];
159 let attributes = attributes(client_final)?;
160 reject_mandatory_extension(&attributes)?;
161 if required(&attributes, b'r')? != self.combined_nonce {
162 return Err(invalid("SCRAM nonce mismatch"));
163 }
164 let channel_binding = STANDARD
165 .decode(required(&attributes, b'c')?)
166 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
167 if !bool::from(channel_binding.ct_eq(&self.expected_channel_binding)) {
168 return Err(invalid("SCRAM channel binding mismatch"));
169 }
170 let encoded_proof = required(&attributes, b'p')?;
171 if proof_marker + 3 + encoded_proof.len() != client_final.len() {
172 return Err(invalid("SCRAM proof is not the final attribute"));
173 }
174 let proof = STANDARD
175 .decode(encoded_proof)
176 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
177 if proof.len() != 32 {
178 return Err(invalid("SCRAM proof is not 32 bytes"));
179 }
180
181 let auth_message = format!(
182 "{},{},{}",
183 self.client_first_bare, self.server_first, without_proof
184 );
185 let client_key = hmac(&self.salted_password, b"Client Key");
186 let stored_key = Sha256::digest(client_key);
187 let client_signature = hmac(&stored_key, auth_message.as_bytes());
188 let mut recovered_key = [0; 32];
189 for ((recovered, proof), signature) in
190 recovered_key.iter_mut().zip(proof).zip(client_signature)
191 {
192 *recovered = proof ^ signature;
193 }
194 let recovered_stored_key = Sha256::digest(recovered_key);
195 if !bool::from(recovered_stored_key.ct_eq(&stored_key)) {
196 return Err(invalid("invalid SCRAM client proof"));
197 }
198
199 let server_key = hmac(&self.salted_password, b"Server Key");
200 let server_signature = hmac(&server_key, auth_message.as_bytes());
201 Ok(Bytes::from(format!(
202 "v={}",
203 STANDARD.encode(server_signature)
204 )))
205 }
206}
207
208fn split_gs2(message: &str) -> io::Result<(&str, &str)> {
209 let first = message
210 .find(',')
211 .ok_or_else(|| invalid("malformed SCRAM GS2 header"))?;
212 let second = message[first + 1..]
213 .find(',')
214 .map(|position| first + 1 + position)
215 .ok_or_else(|| invalid("malformed SCRAM GS2 header"))?;
216 Ok((&message[..=second], &message[second + 1..]))
217}
218
219fn attributes(message: &str) -> io::Result<Vec<(u8, &str)>> {
220 let mut output = Vec::new();
221 for attribute in message.split(',') {
222 let bytes = attribute.as_bytes();
223 if bytes.len() < 2 || bytes[1] != b'=' || !bytes[0].is_ascii_alphabetic() {
224 return Err(invalid("malformed SCRAM attribute"));
225 }
226 if output.iter().any(|(name, _)| *name == bytes[0]) {
227 return Err(invalid("duplicate SCRAM attribute"));
228 }
229 output.push((bytes[0], &attribute[2..]));
230 }
231 Ok(output)
232}
233
234fn required<'a>(attributes: &[(u8, &'a str)], name: u8) -> io::Result<&'a str> {
235 attributes
236 .iter()
237 .find_map(|(candidate, value)| (*candidate == name).then_some(*value))
238 .ok_or_else(|| invalid("required SCRAM attribute is missing"))
239}
240
241fn reject_mandatory_extension(attributes: &[(u8, &str)]) -> io::Result<()> {
242 if attributes.iter().any(|(name, _)| *name == b'm') {
243 Err(invalid("unsupported mandatory SCRAM extension"))
244 } else {
245 Ok(())
246 }
247}
248
249fn validate_nonce(nonce: &str) -> io::Result<()> {
250 if !nonce.is_empty()
251 && nonce
252 .bytes()
253 .all(|byte| matches!(byte, 0x21..=0x2b | 0x2d..=0x7e))
254 {
255 Ok(())
256 } else {
257 Err(invalid("invalid SCRAM nonce"))
258 }
259}
260
261fn random_nonce() -> String {
262 let mut rng = rand::rng();
263 (0..24)
264 .map(|_| {
265 let mut byte = rng.random_range(0x21_u8..0x7f);
266 if byte == b',' {
267 byte = b'~';
268 }
269 char::from(byte)
270 })
271 .collect()
272}
273
274fn normalize(password: &[u8]) -> Vec<u8> {
275 let Ok(password) = str::from_utf8(password) else {
276 return password.to_vec();
277 };
278 stringprep::saslprep(password).map_or_else(
279 |_| password.as_bytes().to_vec(),
280 |normalised| normalised.into_owned().into_bytes(),
281 )
282}
283
284fn hi(password: &[u8], salt: &[u8], iterations: u32) -> [u8; 32] {
285 let mut input = Vec::with_capacity(salt.len() + 4);
286 input.extend_from_slice(salt);
287 input.extend_from_slice(&[0, 0, 0, 1]);
288 let mut previous = hmac(password, &input);
289 let mut output = previous;
290 for _ in 1..iterations {
291 previous = hmac(password, &previous);
292 for (output, previous) in output.iter_mut().zip(previous) {
293 *output ^= previous;
294 }
295 }
296 output
297}
298
299fn hmac(key: &[u8], input: &[u8]) -> [u8; 32] {
300 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts every key length");
301 mac.update(input);
302 mac.finalize().into_bytes().into()
303}
304
305fn invalid(message: &'static str) -> io::Error {
306 io::Error::new(io::ErrorKind::InvalidInput, message)
307}
308
309#[cfg(test)]
310mod tests {
311 use postgres_protocol::authentication::sasl::{ChannelBinding, ScramSha256};
312
313 use super::*;
314
315 fn exchange(binding: ServerChannelBinding, client_binding: ChannelBinding, mechanism: &[u8]) {
316 let server = ScramServer::with_parameters(
317 b"pencil",
318 b"fixed test salt".to_vec(),
319 DEFAULT_ITERATIONS,
320 binding,
321 )
322 .unwrap();
323 let mut client = ScramSha256::new(b"pencil", client_binding);
324 let (exchange, server_first) = server.start(mechanism, client.message()).unwrap();
325 client.update(&server_first).unwrap();
326 let server_final = exchange.finish(client.message()).unwrap();
327 client.finish(&server_final).unwrap();
328 }
329
330 #[test]
331 fn verifies_scram_sha_256() {
332 exchange(
333 ServerChannelBinding::None,
334 ChannelBinding::unsupported(),
335 SCRAM_SHA_256,
336 );
337 }
338
339 #[test]
340 fn verifies_scram_sha_256_plus() {
341 let binding = b"certificate digest".to_vec();
342 exchange(
343 ServerChannelBinding::TlsServerEndPoint(binding.clone()),
344 ChannelBinding::tls_server_end_point(binding),
345 SCRAM_SHA_256_PLUS,
346 );
347 }
348
349 #[test]
350 fn rejects_wrong_password_and_channel_binding() {
351 let server = ScramServer::with_parameters(
352 b"correct",
353 b"fixed test salt".to_vec(),
354 DEFAULT_ITERATIONS,
355 ServerChannelBinding::None,
356 )
357 .unwrap();
358 let mut client = ScramSha256::new(b"wrong", ChannelBinding::unsupported());
359 let (exchange, server_first) = server.start(SCRAM_SHA_256, client.message()).unwrap();
360 client.update(&server_first).unwrap();
361 assert!(exchange.finish(client.message()).is_err());
362
363 let server = ScramServer::with_parameters(
364 b"correct",
365 b"fixed test salt".to_vec(),
366 DEFAULT_ITERATIONS,
367 ServerChannelBinding::TlsServerEndPoint(b"expected".to_vec()),
368 )
369 .unwrap();
370 let client = ScramSha256::new(
371 b"correct",
372 ChannelBinding::tls_server_end_point(b"different".to_vec()),
373 );
374 let (exchange, server_first) = server.start(SCRAM_SHA_256_PLUS, client.message()).unwrap();
375 let mut client = client;
376 client.update(&server_first).unwrap();
377 assert!(exchange.finish(client.message()).is_err());
378 }
379}