sqlmodel_postgres/auth/
scram.rs1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
4use hmac::{Hmac, KeyInit, Mac};
5use rand::rand_core::UnwrapErr;
6use rand::{RngExt, distr::Alphanumeric, rngs::SysRng};
7use sha2::{Digest, Sha256};
8use sqlmodel_core::Error;
9use sqlmodel_core::error::{ConnectionError, ConnectionErrorKind, ProtocolError};
10use subtle::ConstantTimeEq;
11
12type HmacSha256 = Hmac<Sha256>;
13
14pub struct ScramClient {
15 username: String,
16 password: String,
17 client_nonce: String,
18
19 server_nonce: Option<String>,
21 salt: Option<Vec<u8>>,
22 iterations: Option<u32>,
23
24 salted_password: Option<[u8; 32]>,
26 auth_message: Option<String>,
27}
28
29impl ScramClient {
30 pub fn new(username: &str, password: &str) -> Self {
31 let client_nonce: String = UnwrapErr(SysRng)
37 .sample_iter(&Alphanumeric)
38 .take(32)
39 .map(char::from)
40 .collect();
41
42 Self {
43 username: username.to_string(),
44 password: password.to_string(),
45 client_nonce,
46 server_nonce: None,
47 salt: None,
48 iterations: None,
49 salted_password: None,
50 auth_message: None,
51 }
52 }
53
54 pub fn client_first(&self) -> Vec<u8> {
56 format!("n,,n={},r={}", self.username, self.client_nonce).into_bytes()
61 }
62
63 #[allow(clippy::result_large_err)]
65 pub fn process_server_first(&mut self, data: &[u8]) -> Result<Vec<u8>, Error> {
66 let msg = std::str::from_utf8(data)
67 .map_err(|e| protocol_error(format!("Invalid UTF-8 in SASL continue: {}", e)))?;
68
69 let mut combined_nonce = None;
71 let mut salt = None;
72 let mut iterations = None;
73
74 for part in msg.split(',') {
75 if let Some(value) = part.strip_prefix("r=") {
76 combined_nonce = Some(value.to_string());
77 } else if let Some(value) = part.strip_prefix("s=") {
78 salt = Some(
79 BASE64
80 .decode(value)
81 .map_err(|e| protocol_error(format!("Invalid base64 salt: {}", e)))?,
82 );
83 } else if let Some(value) = part.strip_prefix("i=") {
84 iterations = Some(
85 value
86 .parse()
87 .map_err(|e| protocol_error(format!("Invalid iterations: {}", e)))?,
88 );
89 }
90 }
91
92 let combined_nonce = combined_nonce.ok_or_else(|| protocol_error("Missing nonce"))?;
93 let salt = salt.ok_or_else(|| protocol_error("Missing salt"))?;
94 let iterations = iterations.ok_or_else(|| protocol_error("Missing iterations"))?;
95
96 if !combined_nonce.starts_with(&self.client_nonce) {
98 return Err(protocol_error("Invalid server nonce"));
99 }
100
101 let mut salted_password = [0u8; 32];
103 pbkdf2::pbkdf2::<HmacSha256>(
104 self.password.as_bytes(),
105 &salt,
106 iterations,
107 &mut salted_password,
108 )
109 .map_err(|e| protocol_error(format!("PBKDF2 failed: {}", e)))?;
110
111 let client_first_bare = format!("n={},r={}", self.username, self.client_nonce);
113 let client_final_without_proof = format!("c=biws,r={}", combined_nonce); let auth_message = format!(
115 "{},{},{}",
116 client_first_bare, msg, client_final_without_proof
117 );
118
119 let client_key = hmac_sha256(&salted_password, b"Client Key")?;
121 let stored_key = sha256(&client_key);
122 let client_signature = hmac_sha256(&stored_key, auth_message.as_bytes())?;
123
124 let client_proof: Vec<u8> = client_key
125 .iter()
126 .zip(client_signature.iter())
127 .map(|(a, b)| a ^ b)
128 .collect();
129
130 self.server_nonce = Some(combined_nonce.clone());
132 self.salted_password = Some(salted_password);
133 self.auth_message = Some(auth_message);
134 self.salt = Some(salt);
135 self.iterations = Some(iterations);
136
137 let client_final = format!(
139 "c=biws,r={},p={}",
140 combined_nonce,
141 BASE64.encode(&client_proof)
142 );
143
144 Ok(client_final.into_bytes())
145 }
146
147 #[allow(clippy::result_large_err)]
149 pub fn verify_server_final(&self, data: &[u8]) -> Result<(), Error> {
150 let msg = std::str::from_utf8(data)
151 .map_err(|e| protocol_error(format!("Invalid UTF-8 in SASL final: {}", e)))?;
152
153 let server_signature_b64 = msg
154 .strip_prefix("v=")
155 .ok_or_else(|| protocol_error("Invalid server-final format"))?;
156
157 let server_signature = BASE64
158 .decode(server_signature_b64)
159 .map_err(|e| protocol_error(format!("Invalid base64 server signature: {}", e)))?;
160
161 let salted_password = self
163 .salted_password
164 .as_ref()
165 .ok_or_else(|| protocol_error("Missing salted password state"))?;
166 let auth_message = self
167 .auth_message
168 .as_ref()
169 .ok_or_else(|| protocol_error("Missing auth message state"))?;
170
171 let server_key = hmac_sha256(salted_password, b"Server Key")?;
172 let expected_signature = hmac_sha256(&server_key, auth_message.as_bytes())?;
173
174 if server_signature.ct_eq(&expected_signature).into() {
178 Ok(())
179 } else {
180 Err(auth_error("Server signature mismatch"))
181 }
182 }
183}
184
185fn protocol_error(msg: impl Into<String>) -> Error {
188 Error::Protocol(ProtocolError {
189 message: msg.into(),
190 raw_data: None,
191 source: None,
192 })
193}
194
195fn auth_error(msg: impl Into<String>) -> Error {
196 Error::Connection(ConnectionError {
197 kind: ConnectionErrorKind::Authentication,
198 message: msg.into(),
199 source: None,
200 })
201}
202
203#[allow(clippy::result_large_err)]
204fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<[u8; 32], Error> {
205 let mut mac = HmacSha256::new_from_slice(key)
206 .map_err(|e| protocol_error(format!("HMAC init failed: {}", e)))?;
207 mac.update(data);
208 let result = mac.finalize();
209 let bytes = result.into_bytes();
210 Ok(bytes.into())
211}
212
213fn sha256(data: &[u8]) -> [u8; 32] {
214 let mut hasher = Sha256::new();
215 hasher.update(data);
216 hasher.finalize().into()
217}