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 : pub use exchange::{exchange, Exchange};
16 : pub use key::ScramKey;
17 : pub use secret::ServerSecret;
18 :
19 : use hmac::{Hmac, Mac};
20 : use sha2::{Digest, Sha256};
21 :
22 : const SCRAM_SHA_256: &str = "SCRAM-SHA-256";
23 : const SCRAM_SHA_256_PLUS: &str = "SCRAM-SHA-256-PLUS";
24 :
25 : /// A list of supported SCRAM methods.
26 : pub const METHODS: &[&str] = &[SCRAM_SHA_256_PLUS, SCRAM_SHA_256];
27 : pub const METHODS_WITHOUT_PLUS: &[&str] = &[SCRAM_SHA_256];
28 :
29 : /// Decode base64 into array without any heap allocations
30 98 : fn base64_decode_array<const N: usize>(input: impl AsRef<[u8]>) -> Option<[u8; N]> {
31 98 : let mut bytes = [0u8; N];
32 :
33 98 : let size = base64::decode_config_slice(input, base64::STANDARD, &mut bytes).ok()?;
34 98 : if size != N {
35 0 : return None;
36 98 : }
37 98 :
38 98 : Some(bytes)
39 98 : }
40 :
41 : /// This function essentially is `Hmac(sha256, key, input)`.
42 : /// Further reading: <https://datatracker.ietf.org/doc/html/rfc2104>.
43 30 : fn hmac_sha256<'a>(key: &[u8], parts: impl IntoIterator<Item = &'a [u8]>) -> [u8; 32] {
44 30 : let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("bad key size");
45 150 : parts.into_iter().for_each(|s| mac.update(s));
46 30 :
47 30 : mac.finalize().into_bytes().into()
48 30 : }
49 :
50 24 : fn sha256<'a>(parts: impl IntoIterator<Item = &'a [u8]>) -> [u8; 32] {
51 24 : let mut hasher = Sha256::new();
52 24 : parts.into_iter().for_each(|s| hasher.update(s));
53 24 :
54 24 : hasher.finalize().into()
55 24 : }
56 :
57 : #[cfg(test)]
58 : mod tests {
59 : use crate::sasl::{Mechanism, Step};
60 :
61 : use super::{Exchange, ServerSecret};
62 :
63 : #[test]
64 2 : fn snapshot() {
65 2 : let iterations = 4096;
66 2 : let salt = "QSXCR+Q6sek8bf92";
67 2 : let stored_key = "FO+9jBb3MUukt6jJnzjPZOWc5ow/Pu6JtPyju0aqaE8=";
68 2 : let server_key = "qxJ1SbmSAi5EcS0J5Ck/cKAm/+Ixa+Kwp63f4OHDgzo=";
69 2 : let secret = format!("SCRAM-SHA-256${iterations}:{salt}${stored_key}:{server_key}",);
70 2 : let secret = ServerSecret::parse(&secret).unwrap();
71 2 :
72 2 : const NONCE: [u8; 18] = [
73 2 : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
74 2 : ];
75 2 : let mut exchange = Exchange::new(
76 2 : &secret,
77 2 : || NONCE,
78 2 : crate::config::TlsServerEndPoint::Undefined,
79 2 : );
80 2 :
81 2 : let client_first = "n,,n=user,r=rOprNGfwEbeRWgbNEkqO";
82 2 : let client_final = "c=biws,r=rOprNGfwEbeRWgbNEkqOAQIDBAUGBwgJCgsMDQ4PEBES,p=rw1r5Kph5ThxmaUBC2GAQ6MfXbPnNkFiTIvdb/Rear0=";
83 2 : let server_first =
84 2 : "r=rOprNGfwEbeRWgbNEkqOAQIDBAUGBwgJCgsMDQ4PEBES,s=QSXCR+Q6sek8bf92,i=4096";
85 2 : let server_final = "v=qtUDIofVnIhM7tKn93EQUUt5vgMOldcDVu1HC+OH0o0=";
86 :
87 2 : exchange = match exchange.exchange(client_first).unwrap() {
88 2 : Step::Continue(exchange, message) => {
89 2 : assert_eq!(message, server_first);
90 2 : exchange
91 : }
92 0 : Step::Success(_, _) => panic!("expected continue, got success"),
93 0 : Step::Failure(f) => panic!("{f}"),
94 : };
95 :
96 2 : let key = match exchange.exchange(client_final).unwrap() {
97 2 : Step::Success(key, message) => {
98 2 : assert_eq!(message, server_final);
99 2 : key
100 : }
101 0 : Step::Continue(_, _) => panic!("expected success, got continue"),
102 0 : Step::Failure(f) => panic!("{f}"),
103 : };
104 :
105 2 : assert_eq!(
106 2 : key.as_bytes(),
107 2 : [
108 2 : 74, 103, 1, 132, 12, 31, 200, 48, 28, 54, 82, 232, 207, 12, 138, 189, 40, 32, 134,
109 2 : 27, 125, 170, 232, 35, 171, 167, 166, 41, 70, 228, 182, 112,
110 2 : ]
111 2 : );
112 2 : }
113 :
114 4 : async fn run_round_trip_test(server_password: &str, client_password: &str) {
115 12 : let scram_secret = ServerSecret::build(server_password).await.unwrap();
116 4 : let outcome = super::exchange(&scram_secret, client_password.as_bytes())
117 12 : .await
118 4 : .unwrap();
119 4 :
120 4 : match outcome {
121 2 : crate::sasl::Outcome::Success(_) => {}
122 2 : crate::sasl::Outcome::Failure(r) => panic!("{r}"),
123 : }
124 2 : }
125 :
126 : #[tokio::test]
127 2 : async fn round_trip() {
128 12 : run_round_trip_test("pencil", "pencil").await
129 2 : }
130 :
131 : #[tokio::test]
132 : #[should_panic(expected = "password doesn't match")]
133 2 : async fn failure() {
134 12 : run_round_trip_test("pencil", "eraser").await
135 2 : }
136 : }
|