TLA Line data Source code
1 : //! Salted Challenge Response Authentication Mechanism.
2 : //!
3 : //! RFC: <https://datatracker.ietf.org/doc/html/rfc5802>.
4 : //!
5 : //! Reference implementation:
6 : //! * <https://github.com/postgres/postgres/blob/94226d4506e66d6e7cbf4b391f1e7393c1962841/src/backend/libpq/auth-scram.c>
7 : //! * <https://github.com/postgres/postgres/blob/94226d4506e66d6e7cbf4b391f1e7393c1962841/src/interfaces/libpq/fe-auth-scram.c>
8 :
9 : mod exchange;
10 : mod key;
11 : mod messages;
12 : mod secret;
13 : mod signature;
14 :
15 : #[cfg(any(test, doc))]
16 : mod password;
17 :
18 : pub use exchange::{exchange, Exchange};
19 : pub use key::ScramKey;
20 : pub use secret::ServerSecret;
21 :
22 : use hmac::{Hmac, Mac};
23 : use sha2::{Digest, Sha256};
24 :
25 : const SCRAM_SHA_256: &str = "SCRAM-SHA-256";
26 : const SCRAM_SHA_256_PLUS: &str = "SCRAM-SHA-256-PLUS";
27 :
28 : /// A list of supported SCRAM methods.
29 : pub const METHODS: &[&str] = &[SCRAM_SHA_256_PLUS, SCRAM_SHA_256];
30 : pub const METHODS_WITHOUT_PLUS: &[&str] = &[SCRAM_SHA_256];
31 :
32 : /// Decode base64 into array without any heap allocations
33 CBC 288 : fn base64_decode_array<const N: usize>(input: impl AsRef<[u8]>) -> Option<[u8; N]> {
34 288 : let mut bytes = [0u8; N];
35 :
36 288 : let size = base64::decode_config_slice(input, base64::STANDARD, &mut bytes).ok()?;
37 288 : if size != N {
38 UBC 0 : return None;
39 CBC 288 : }
40 288 :
41 288 : Some(bytes)
42 288 : }
43 :
44 : /// This function essentially is `Hmac(sha256, key, input)`.
45 : /// Further reading: <https://datatracker.ietf.org/doc/html/rfc2104>.
46 4208 : fn hmac_sha256<'a>(key: &[u8], parts: impl IntoIterator<Item = &'a [u8]>) -> [u8; 32] {
47 4208 : let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("bad key size");
48 4553 : parts.into_iter().for_each(|s| mac.update(s));
49 4208 :
50 4208 : mac.finalize().into_bytes().into()
51 4208 : }
52 :
53 60 : fn sha256<'a>(parts: impl IntoIterator<Item = &'a [u8]>) -> [u8; 32] {
54 60 : let mut hasher = Sha256::new();
55 62 : parts.into_iter().for_each(|s| hasher.update(s));
56 60 :
57 60 : hasher.finalize().into()
58 60 : }
59 :
60 : #[cfg(test)]
61 : mod tests {
62 : use crate::sasl::{Mechanism, Step};
63 :
64 : use super::{password::SaltedPassword, Exchange, ServerSecret};
65 :
66 1 : #[test]
67 1 : fn happy_path() {
68 1 : let iterations = 4096;
69 1 : let salt_base64 = "QSXCR+Q6sek8bf92";
70 1 : let pw = SaltedPassword::new(
71 1 : b"pencil",
72 1 : base64::decode(salt_base64).unwrap().as_slice(),
73 1 : iterations,
74 1 : );
75 1 :
76 1 : let secret = ServerSecret {
77 1 : iterations,
78 1 : salt_base64: salt_base64.to_owned(),
79 1 : stored_key: pw.client_key().sha256(),
80 1 : server_key: pw.server_key(),
81 1 : doomed: false,
82 1 : };
83 1 : const NONCE: [u8; 18] = [
84 1 : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
85 1 : ];
86 1 : let mut exchange = Exchange::new(
87 1 : &secret,
88 1 : || NONCE,
89 1 : crate::config::TlsServerEndPoint::Undefined,
90 1 : );
91 1 :
92 1 : let client_first = "n,,n=user,r=rOprNGfwEbeRWgbNEkqO";
93 1 : let client_final = "c=biws,r=rOprNGfwEbeRWgbNEkqOAQIDBAUGBwgJCgsMDQ4PEBES,p=rw1r5Kph5ThxmaUBC2GAQ6MfXbPnNkFiTIvdb/Rear0=";
94 1 : let server_first =
95 1 : "r=rOprNGfwEbeRWgbNEkqOAQIDBAUGBwgJCgsMDQ4PEBES,s=QSXCR+Q6sek8bf92,i=4096";
96 1 : let server_final = "v=qtUDIofVnIhM7tKn93EQUUt5vgMOldcDVu1HC+OH0o0=";
97 :
98 1 : exchange = match exchange.exchange(client_first).unwrap() {
99 1 : Step::Continue(exchange, message) => {
100 1 : assert_eq!(message, server_first);
101 1 : exchange
102 : }
103 UBC 0 : Step::Success(_, _) => panic!("expected continue, got success"),
104 0 : Step::Failure(f) => panic!("{f}"),
105 : };
106 :
107 CBC 1 : let key = match exchange.exchange(client_final).unwrap() {
108 1 : Step::Success(key, message) => {
109 1 : assert_eq!(message, server_final);
110 1 : key
111 : }
112 UBC 0 : Step::Continue(_, _) => panic!("expected success, got continue"),
113 0 : Step::Failure(f) => panic!("{f}"),
114 : };
115 :
116 CBC 1 : assert_eq!(
117 1 : key.as_bytes(),
118 1 : [
119 1 : 74, 103, 1, 132, 12, 31, 200, 48, 28, 54, 82, 232, 207, 12, 138, 189, 40, 32, 134,
120 1 : 27, 125, 170, 232, 35, 171, 167, 166, 41, 70, 228, 182, 112,
121 1 : ]
122 1 : );
123 1 : }
124 : }
|