1use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use subtle::ConstantTimeEq;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MysqlCachingSha2Verifier {
21 stage2: [u8; 32],
22}
23
24impl MysqlCachingSha2Verifier {
25 pub fn from_password(password: &str) -> Self {
26 let stage1 = Sha256::digest(password.as_bytes());
27 Self {
28 stage2: Sha256::digest(stage1).into(),
29 }
30 }
31
32 pub fn verify(&self, nonce: &[u8], proof: &[u8]) -> bool {
33 if proof.is_empty() {
34 return bool::from(self.stage2.ct_eq(&Self::from_password("").stage2));
35 }
36 let Ok(proof) = <&[u8; 32]>::try_from(proof) else {
37 return false;
38 };
39 let mask = Sha256::new()
40 .chain_update(self.stage2)
41 .chain_update(nonce)
42 .finalize();
43 let mut stage1 = [0_u8; 32];
44 for (output, (proof, mask)) in stage1.iter_mut().zip(proof.iter().zip(mask)) {
45 *output = proof ^ mask;
46 }
47 let candidate: [u8; 32] = Sha256::digest(stage1).into();
48 bool::from(candidate.ct_eq(&self.stage2))
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct UserEntry {
55 pub id: u64,
57 pub username: String,
59 #[serde(skip_serializing_if = "String::is_empty", default)]
62 pub password_hash: String,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub scram_sha_256: Option<crate::security_hardening::ScramVerifier>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub mysql_caching_sha2: Option<MysqlCachingSha2Verifier>,
71 #[serde(default)]
73 pub roles: Vec<String>,
74 #[serde(default)]
76 pub is_admin: bool,
77 pub created_epoch: u64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct RoleEntry {
84 pub name: String,
86 #[serde(default)]
88 pub permissions: Vec<Permission>,
89 pub created_epoch: u64,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(tag = "kind", rename_all = "snake_case")]
96pub enum Permission {
97 All,
99 Select { table: String },
101 Insert { table: String },
103 Update { table: String },
105 Delete { table: String },
107 SelectColumns { table: String, columns: Vec<String> },
109 InsertColumns { table: String, columns: Vec<String> },
111 UpdateColumns { table: String, columns: Vec<String> },
113 Ddl,
115 Admin,
117}
118
119impl Permission {
120 pub fn satisfies(&self, required: &Permission) -> bool {
128 match (self, required) {
129 (Permission::All, Permission::Admin) => false,
131 (Permission::All, _) => true,
132 (Permission::Admin, Permission::Admin) => true,
133 (Permission::Ddl, Permission::Ddl) => true,
134 (Permission::Select { table: a }, Permission::Select { table: b }) => a == b,
135 (Permission::Insert { table: a }, Permission::Insert { table: b }) => a == b,
136 (Permission::Update { table: a }, Permission::Update { table: b }) => a == b,
137 (Permission::Delete { table: a }, Permission::Delete { table: b }) => a == b,
138 (
139 Permission::SelectColumns {
140 table: a,
141 columns: granted,
142 },
143 Permission::SelectColumns {
144 table: b,
145 columns: required,
146 },
147 )
148 | (
149 Permission::InsertColumns {
150 table: a,
151 columns: granted,
152 },
153 Permission::InsertColumns {
154 table: b,
155 columns: required,
156 },
157 )
158 | (
159 Permission::UpdateColumns {
160 table: a,
161 columns: granted,
162 },
163 Permission::UpdateColumns {
164 table: b,
165 columns: required,
166 },
167 ) => a == b && required.iter().all(|column| granted.contains(column)),
168 _ => false,
169 }
170 }
171}
172
173impl std::fmt::Display for Permission {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 match self {
176 Permission::All => write!(f, "ALL"),
177 Permission::Admin => write!(f, "ADMIN"),
178 Permission::Ddl => write!(f, "DDL"),
179 Permission::Select { table } => write!(f, "SELECT ON {table}"),
180 Permission::Insert { table } => write!(f, "INSERT ON {table}"),
181 Permission::Update { table } => write!(f, "UPDATE ON {table}"),
182 Permission::Delete { table } => write!(f, "DELETE ON {table}"),
183 Permission::SelectColumns { table, columns } => {
184 write!(f, "SELECT ({}) ON {table}", columns.join(", "))
185 }
186 Permission::InsertColumns { table, columns } => {
187 write!(f, "INSERT ({}) ON {table}", columns.join(", "))
188 }
189 Permission::UpdateColumns { table, columns } => {
190 write!(f, "UPDATE ({}) ON {table}", columns.join(", "))
191 }
192 }
193 }
194}
195
196#[derive(Debug, Clone)]
199pub struct Principal {
200 pub user_id: u64,
202 pub created_epoch: u64,
204 pub username: String,
205 pub is_admin: bool,
206 pub roles: Vec<String>,
207 pub permissions: Vec<Permission>,
209}
210
211impl Principal {
212 pub fn has_permission(&self, required: &Permission) -> bool {
214 if self.is_admin {
215 return true;
216 }
217 self.permissions.iter().any(|p| p.satisfies(required))
218 }
219
220 pub fn column_access(&self, table: &str, operation: ColumnOperation) -> ColumnAccess {
221 if self.is_admin
222 || self
223 .permissions
224 .iter()
225 .any(|permission| matches!(permission, Permission::All))
226 {
227 return ColumnAccess::All;
228 }
229 let full = self
230 .permissions
231 .iter()
232 .any(|permission| match (operation, permission) {
233 (ColumnOperation::Select, Permission::Select { table: granted })
234 | (ColumnOperation::Insert, Permission::Insert { table: granted })
235 | (ColumnOperation::Update, Permission::Update { table: granted }) => {
236 granted == table
237 }
238 _ => false,
239 });
240 if full {
241 return ColumnAccess::All;
242 }
243 let mut columns = Vec::new();
244 for permission in &self.permissions {
245 let grant = match (operation, permission) {
246 (
247 ColumnOperation::Select,
248 Permission::SelectColumns {
249 table: granted,
250 columns,
251 },
252 )
253 | (
254 ColumnOperation::Insert,
255 Permission::InsertColumns {
256 table: granted,
257 columns,
258 },
259 )
260 | (
261 ColumnOperation::Update,
262 Permission::UpdateColumns {
263 table: granted,
264 columns,
265 },
266 ) if granted == table => Some(columns),
267 _ => None,
268 };
269 if let Some(grant) = grant {
270 for column in grant {
271 if !columns.contains(column) {
272 columns.push(column.clone());
273 }
274 }
275 }
276 }
277 if columns.is_empty() {
278 ColumnAccess::Denied
279 } else {
280 ColumnAccess::Columns(columns)
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum ColumnOperation {
287 Select,
288 Insert,
289 Update,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub enum ColumnAccess {
294 All,
295 Columns(Vec<String>),
296 Denied,
297}
298
299pub fn hash_password(password: &str) -> Result<String, String> {
307 use argon2::{
308 password_hash::{PasswordHasher, SaltString},
309 Algorithm, Argon2, Version,
310 };
311 use getrandom::getrandom;
312 let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
314 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
315 let mut salt_bytes = [0u8; 32];
317 getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
318 let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
319 let hash = argon2
320 .hash_password(password.as_bytes(), &salt)
321 .map_err(|e| e.to_string())?;
322 Ok(hash.to_string())
323}
324
325pub fn scram_verifier(password: &str) -> Result<crate::security_hardening::ScramVerifier, String> {
327 let mut salt = [0_u8; 16];
328 getrandom::getrandom(&mut salt).map_err(|error| error.to_string())?;
329 crate::security_hardening::ScramVerifier::from_password(
330 password,
331 &salt,
332 crate::security_hardening::SCRAM_SHA_256_MIN_ITERATIONS,
333 )
334 .map_err(|error| error.to_string())
335}
336
337pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
340 use argon2::{password_hash::PasswordVerifier, Argon2};
341 let parsed_hash =
342 argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
343 Ok(Argon2::default()
344 .verify_password(password.as_bytes(), &parsed_hash)
345 .is_ok())
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn password_hash_round_trip() {
354 let password = "correct horse battery staple";
355 let hash = hash_password(password).unwrap();
356 assert!(verify_password(password, &hash).unwrap());
357 assert!(!verify_password("wrong password", &hash).unwrap());
358 }
359
360 #[test]
361 fn caching_sha2_verifier_accepts_only_matching_proof() {
362 let verifier = MysqlCachingSha2Verifier::from_password("password");
363 let nonce = b"12345678901234567890";
364 let stage1 = Sha256::digest(b"password");
365 let stage2 = Sha256::digest(stage1);
366 let mask = Sha256::new()
367 .chain_update(stage2)
368 .chain_update(nonce)
369 .finalize();
370 let proof = stage1
371 .iter()
372 .zip(mask)
373 .map(|(left, right)| left ^ right)
374 .collect::<Vec<_>>();
375 assert!(verifier.verify(nonce, &proof));
376 assert!(!verifier.verify(nonce, &[0; 32]));
377 }
378
379 #[test]
380 fn permission_satisfies() {
381 assert!(Permission::All.satisfies(&Permission::Ddl));
383 assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
384 assert!(Permission::All.satisfies(&Permission::Insert { table: "t".into() }));
385 assert!(!Permission::All.satisfies(&Permission::Admin));
387 assert!(Permission::Select { table: "t".into() }
389 .satisfies(&Permission::Select { table: "t".into() }));
390 assert!(
391 !Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
392 table: "other".into()
393 })
394 );
395 assert!(!Permission::Select { table: "t".into() }
397 .satisfies(&Permission::Insert { table: "t".into() }));
398 assert!(!Permission::Ddl.satisfies(&Permission::Admin));
400 }
401
402 #[test]
403 fn principal_admin_bypasses_checks() {
404 let principal = Principal {
405 user_id: 0,
406 created_epoch: 0,
407 username: "admin".into(),
408 is_admin: true,
409 roles: vec![],
410 permissions: vec![],
411 };
412 assert!(principal.has_permission(&Permission::Admin));
413 assert!(principal.has_permission(&Permission::Select {
414 table: "anything".into()
415 }));
416 }
417}