1use alloc::collections::{BTreeMap, BTreeSet};
18use alloc::string::{String, ToString};
19use alloc::vec::Vec;
20
21use spg_storage::{ColumnSchema, DataType, Row, Value};
22
23use crate::{Engine, QueryResult};
24
25const SALT_LEN: usize = 16;
26const HASH_LEN: usize = 32;
27pub const MYSQL_NATIVE_HASH_LEN: usize = 20;
30pub const CACHING_SHA2_HASH_LEN: usize = 32;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Role {
37 Admin,
38 ReadWrite,
39 ReadOnly,
40}
41
42impl Role {
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Self::Admin => "admin",
46 Self::ReadWrite => "readwrite",
47 Self::ReadOnly => "readonly",
48 }
49 }
50
51 pub fn parse(s: &str) -> Option<Self> {
52 match s.to_ascii_lowercase().as_str() {
53 "admin" => Some(Self::Admin),
54 "readwrite" | "rw" => Some(Self::ReadWrite),
55 "readonly" | "ro" => Some(Self::ReadOnly),
56 _ => None,
57 }
58 }
59
60 pub const fn can_read(self) -> bool {
62 true
63 }
64
65 pub const fn can_write(self) -> bool {
67 matches!(self, Self::Admin | Self::ReadWrite)
68 }
69
70 pub const fn can_manage_users(self) -> bool {
72 matches!(self, Self::Admin)
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct UserRecord {
78 pub role: Role,
79 salt: [u8; SALT_LEN],
80 hash: [u8; HASH_LEN],
81 scram: Option<ScramSecrets>,
88 mysql_native: Option<[u8; MYSQL_NATIVE_HASH_LEN]>,
97 caching_sha2: Option<[u8; CACHING_SHA2_HASH_LEN]>,
104 pub can_login: bool,
110 pub password_declared: bool,
122 pub inherit: bool,
126 pub superuser: bool,
128}
129
130#[derive(Debug, Clone)]
135pub struct ScramSecrets {
136 pub iters: u32,
137 pub salt: [u8; SCRAM_SALT_LEN],
138 pub stored_key: [u8; HASH_LEN],
139 pub server_key: [u8; HASH_LEN],
140}
141
142pub const SCRAM_SALT_LEN: usize = 16;
143pub const SCRAM_DEFAULT_ITERS: u32 = 4096;
144
145impl UserRecord {
146 pub fn verify(&self, password: &str) -> bool {
147 let candidate = derive_hash(&self.salt, password);
148 constant_time_eq(&candidate, &self.hash)
149 }
150
151 #[must_use]
160 pub fn has_credentials(&self) -> bool {
161 self.can_login && self.password_declared
162 }
163
164 pub const fn scram(&self) -> Option<&ScramSecrets> {
165 self.scram.as_ref()
166 }
167
168 pub const fn mysql_native(&self) -> Option<&[u8; MYSQL_NATIVE_HASH_LEN]> {
172 self.mysql_native.as_ref()
173 }
174
175 pub const fn caching_sha2(&self) -> Option<&[u8; CACHING_SHA2_HASH_LEN]> {
179 self.caching_sha2.as_ref()
180 }
181
182 pub fn verify_caching_sha2_password(&self, scramble: &[u8], client_response: &[u8]) -> bool {
196 let Some(stored) = self.caching_sha2 else {
197 return false;
198 };
199 if client_response.len() != CACHING_SHA2_HASH_LEN {
200 return false;
201 }
202 if scramble.len() != 20 {
203 return false;
204 }
205 let mut buf = [0u8; 20 + CACHING_SHA2_HASH_LEN];
206 buf[..20].copy_from_slice(scramble);
207 buf[20..].copy_from_slice(&stored);
208 let mask = sha256_bytes(&buf);
209 let mut recovered = [0u8; CACHING_SHA2_HASH_LEN];
210 for i in 0..CACHING_SHA2_HASH_LEN {
211 recovered[i] = client_response[i] ^ mask[i];
212 }
213 let candidate = sha256_bytes(&recovered);
214 constant_time_eq(&candidate, &stored)
215 }
216
217 pub fn verify_mysql_native_password(&self, scramble: &[u8], client_response: &[u8]) -> bool {
229 let Some(stored) = self.mysql_native else {
230 return false;
231 };
232 if client_response.len() != MYSQL_NATIVE_HASH_LEN {
233 return false;
234 }
235 if scramble.len() != 20 {
236 return false;
237 }
238 let mut buf = [0u8; 40];
239 buf[..20].copy_from_slice(scramble);
240 buf[20..].copy_from_slice(&stored);
241 let mask = sha1_bytes(&buf);
242 let mut recovered = [0u8; MYSQL_NATIVE_HASH_LEN];
243 for i in 0..MYSQL_NATIVE_HASH_LEN {
244 recovered[i] = client_response[i] ^ mask[i];
245 }
246 let candidate = sha1_bytes(&recovered);
247 constant_time_eq_sha1(&candidate, &stored)
248 }
249}
250
251#[must_use]
255pub fn compute_mysql_native_hash(password: &str) -> [u8; MYSQL_NATIVE_HASH_LEN] {
256 let inner = sha1_bytes(password.as_bytes());
257 sha1_bytes(&inner)
258}
259
260#[must_use]
264pub fn compute_caching_sha2_hash(password: &str) -> [u8; CACHING_SHA2_HASH_LEN] {
265 let inner = sha256_bytes(password.as_bytes());
266 sha256_bytes(&inner)
267}
268
269fn sha1_bytes(input: &[u8]) -> [u8; MYSQL_NATIVE_HASH_LEN] {
270 use sha1::Digest;
271 let digest = sha1::Sha1::digest(input);
272 let mut out = [0u8; MYSQL_NATIVE_HASH_LEN];
273 out.copy_from_slice(&digest);
274 out
275}
276
277fn sha256_bytes(input: &[u8]) -> [u8; CACHING_SHA2_HASH_LEN] {
278 use sha2::Digest;
279 let digest = sha2::Sha256::digest(input);
280 let mut out = [0u8; CACHING_SHA2_HASH_LEN];
281 out.copy_from_slice(&digest);
282 out
283}
284
285#[derive(Debug, Clone, Default)]
286pub struct UserStore {
287 users: BTreeMap<String, UserRecord>,
288 memberships: BTreeMap<String, BTreeSet<String>>,
292}
293
294#[derive(Debug, PartialEq, Eq)]
295pub enum UserError {
296 Exists,
297 NotFound,
298 InvalidRole,
299 EmptyName,
300 EmptyPassword,
301}
302
303impl core::fmt::Display for UserError {
304 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
305 match self {
306 Self::Exists => f.write_str("user already exists"),
307 Self::NotFound => f.write_str("user not found"),
308 Self::InvalidRole => {
309 f.write_str("invalid role (expected admin / readwrite / readonly)")
310 }
311 Self::EmptyName => f.write_str("username must be non-empty"),
312 Self::EmptyPassword => f.write_str("password must be non-empty"),
313 }
314 }
315}
316
317impl UserStore {
318 pub fn new() -> Self {
319 Self::default()
320 }
321
322 pub fn len(&self) -> usize {
323 self.users.len()
324 }
325
326 pub fn is_empty(&self) -> bool {
327 self.users.is_empty()
328 }
329
330 pub fn contains(&self, name: &str) -> bool {
331 self.users.contains_key(name)
332 }
333
334 #[must_use]
342 pub fn get(&self, name: &str) -> Option<&UserRecord> {
343 self.users.get(name)
344 }
345
346 pub fn iter(&self) -> impl Iterator<Item = (&str, &UserRecord)> {
347 self.users.iter().map(|(k, v)| (k.as_str(), v))
348 }
349
350 pub fn create(
351 &mut self,
352 name: &str,
353 password: &str,
354 role: Role,
355 salt: [u8; SALT_LEN],
356 ) -> Result<(), UserError> {
357 if name.is_empty() {
358 return Err(UserError::EmptyName);
359 }
360 if password.is_empty() {
361 return Err(UserError::EmptyPassword);
362 }
363 if self.users.contains_key(name) {
364 return Err(UserError::Exists);
365 }
366 let hash = derive_hash(&salt, password);
367 let mysql_native = Some(compute_mysql_native_hash(password));
368 let caching_sha2 = Some(compute_caching_sha2_hash(password));
369 self.users.insert(
370 name.to_string(),
371 UserRecord {
372 role,
373 salt,
374 hash,
375 scram: None,
376 mysql_native,
377 caching_sha2,
378 can_login: true,
381 password_declared: true,
385 inherit: true,
386 superuser: matches!(role, Role::Admin),
387 },
388 );
389 Ok(())
390 }
391
392 pub fn set_attributes(&mut self, name: &str, can_login: bool, inherit: bool, superuser: bool) {
395 if let Some(r) = self.users.get_mut(name) {
396 r.can_login = can_login;
397 r.inherit = inherit;
398 r.superuser = superuser;
399 }
400 }
401
402 pub fn set_password_declared(&mut self, name: &str, declared: bool) {
405 if let Some(r) = self.users.get_mut(name) {
406 r.password_declared = declared;
407 }
408 }
409
410 pub fn add_member(&mut self, role: &str, member: &str) {
412 self.memberships
413 .entry(member.to_string())
414 .or_default()
415 .insert(role.to_string());
416 }
417
418 pub fn drop_member(&mut self, role: &str, member: &str) {
420 if let Some(set) = self.memberships.get_mut(member) {
421 set.remove(role);
422 if set.is_empty() {
423 self.memberships.remove(member);
424 }
425 }
426 }
427
428 #[must_use]
430 pub fn memberships_of_transitive(&self, member: &str) -> alloc::collections::BTreeSet<String> {
436 let mut seen: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
437 let mut queue: alloc::vec::Vec<String> = alloc::vec![String::from(member)];
438 while let Some(cur) = queue.pop() {
439 for (m, roles) in &self.memberships {
440 if m.eq_ignore_ascii_case(&cur) {
441 for r in roles {
442 if seen.insert(r.to_ascii_lowercase()) {
443 queue.push(r.clone());
444 }
445 }
446 }
447 }
448 }
449 seen
450 }
451
452 pub fn memberships_of(&self, member: &str) -> Vec<String> {
453 self.memberships
454 .get(member)
455 .map(|s| s.iter().cloned().collect())
456 .unwrap_or_default()
457 }
458
459 pub fn all_memberships(&self) -> impl Iterator<Item = (&str, &str)> {
461 self.memberships
462 .iter()
463 .flat_map(|(m, roles)| roles.iter().map(move |r| (m.as_str(), r.as_str())))
464 }
465
466 #[must_use]
471 pub fn effective_roles(&self, role: &str) -> BTreeSet<String> {
472 let mut out = BTreeSet::new();
473 out.insert(role.to_string());
474 let inherits = self.users.get(role).is_none_or(|r| r.inherit);
478 if !inherits {
479 return out;
480 }
481 let mut queue: Vec<String> = self.memberships_of(role);
482 while let Some(r) = queue.pop() {
483 if !out.insert(r.clone()) {
484 continue;
485 }
486 if self.users.get(&r).is_none_or(|rec| rec.inherit) {
489 queue.extend(self.memberships_of(&r));
490 }
491 }
492 out
493 }
494
495 pub fn drop(&mut self, name: &str) -> Result<(), UserError> {
496 self.memberships.remove(name);
497 for set in self.memberships.values_mut() {
498 set.remove(name);
499 }
500 self.users
501 .remove(name)
502 .map(|_| ())
503 .ok_or(UserError::NotFound)
504 }
505
506 pub fn set_password(
517 &mut self,
518 name: &str,
519 password: Option<&str>,
520 salt: [u8; SALT_LEN],
521 ) -> Result<(), UserError> {
522 let rec = self.users.get_mut(name).ok_or(UserError::NotFound)?;
523 match password {
524 Some(p) => {
525 if p.is_empty() {
526 return Err(UserError::EmptyPassword);
527 }
528 rec.salt = salt;
529 rec.hash = derive_hash(&salt, p);
530 rec.mysql_native = Some(compute_mysql_native_hash(p));
531 rec.caching_sha2 = Some(compute_caching_sha2_hash(p));
532 rec.password_declared = true;
533 }
534 None => {
535 let digest = spg_crypto::hash(&salt);
538 let mut anti = [0u8; SALT_LEN];
539 anti.copy_from_slice(&digest[..SALT_LEN]);
540 rec.salt = salt;
541 rec.hash = derive_hash(&anti, "\u{0}unreachable");
542 rec.mysql_native = None;
543 rec.caching_sha2 = None;
544 rec.scram = None;
545 rec.password_declared = false;
546 }
547 }
548 Ok(())
549 }
550
551 pub fn enable_scram(
552 &mut self,
553 name: &str,
554 password: &str,
555 salt: [u8; SCRAM_SALT_LEN],
556 iters: u32,
557 ) -> Result<(), UserError> {
558 let rec = self.users.get_mut(name).ok_or(UserError::NotFound)?;
559 rec.scram = Some(compute_scram_secrets(password, salt, iters));
560 Ok(())
561 }
562
563 pub fn verify(&self, name: &str, password: &str) -> Option<Role> {
564 let rec = self.users.get(name)?;
565 if rec.verify(password) {
566 Some(rec.role)
567 } else {
568 None
569 }
570 }
571}
572
573fn derive_hash(salt: &[u8; SALT_LEN], password: &str) -> [u8; HASH_LEN] {
574 let mut buf = Vec::with_capacity(SALT_LEN + password.len());
575 buf.extend_from_slice(salt);
576 buf.extend_from_slice(password.as_bytes());
577 spg_crypto::hash(&buf)
578}
579
580pub fn compute_scram_secrets(
591 password: &str,
592 salt: [u8; SCRAM_SALT_LEN],
593 iters: u32,
594) -> ScramSecrets {
595 let salted = spg_crypto::pbkdf2::pbkdf2_sha256_32(password.as_bytes(), &salt, iters);
596 let client_key = spg_crypto::hmac::hmac_sha256(&salted, b"Client Key");
597 let stored_key = spg_crypto::sha256::hash(&client_key);
598 let server_key = spg_crypto::hmac::hmac_sha256(&salted, b"Server Key");
599 ScramSecrets {
600 iters,
601 salt,
602 stored_key,
603 server_key,
604 }
605}
606
607fn constant_time_eq(a: &[u8; HASH_LEN], b: &[u8; HASH_LEN]) -> bool {
610 let mut diff: u8 = 0;
611 for i in 0..HASH_LEN {
612 diff |= a[i] ^ b[i];
613 }
614 diff == 0
615}
616
617fn constant_time_eq_sha1(a: &[u8; MYSQL_NATIVE_HASH_LEN], b: &[u8; MYSQL_NATIVE_HASH_LEN]) -> bool {
620 let mut diff: u8 = 0;
621 for i in 0..MYSQL_NATIVE_HASH_LEN {
622 diff |= a[i] ^ b[i];
623 }
624 diff == 0
625}
626
627const SCRAM_FORMAT_MARKER: u8 = 0xff;
652const MYSQL_NATIVE_FORMAT_MARKER: u8 = 0xfe;
656const CACHING_SHA2_FORMAT_MARKER: u8 = 0xfd;
661const ROLE_ATTRS_FORMAT_MARKER: u8 = 0xfc;
665const PASSWORD_DECLARED_FORMAT_MARKER: u8 = 0xfb;
667
668pub(crate) fn serialize_users(store: &UserStore) -> Vec<u8> {
669 let per_user_floor = 2 + 16 + 1 + SALT_LEN + HASH_LEN + 1 + 1;
670 let mut out = Vec::with_capacity(1 + 4 + store.len() * per_user_floor);
671 out.push(PASSWORD_DECLARED_FORMAT_MARKER);
675 out.extend_from_slice(
676 &u32::try_from(store.users.len())
677 .expect("≤ 4G users")
678 .to_le_bytes(),
679 );
680 for (name, rec) in &store.users {
681 let nl = u16::try_from(name.len()).expect("≤ 65k name");
682 out.extend_from_slice(&nl.to_le_bytes());
683 out.extend_from_slice(name.as_bytes());
684 out.push(match rec.role {
685 Role::Admin => 0,
686 Role::ReadWrite => 1,
687 Role::ReadOnly => 2,
688 });
689 out.extend_from_slice(&rec.salt);
690 out.extend_from_slice(&rec.hash);
691 match &rec.scram {
692 None => out.push(0),
693 Some(s) => {
694 out.push(1);
695 out.extend_from_slice(&s.iters.to_le_bytes());
696 out.extend_from_slice(&s.salt);
697 out.extend_from_slice(&s.stored_key);
698 out.extend_from_slice(&s.server_key);
699 }
700 }
701 match &rec.mysql_native {
702 None => out.push(0),
703 Some(h) => {
704 out.push(1);
705 out.extend_from_slice(h);
706 }
707 }
708 match &rec.caching_sha2 {
709 None => out.push(0),
710 Some(h) => {
711 out.push(1);
712 out.extend_from_slice(h);
713 }
714 }
715 out.push(u8::from(rec.can_login));
717 out.push(u8::from(rec.inherit));
718 out.push(u8::from(rec.superuser));
719 out.push(u8::from(rec.password_declared));
721 }
722 let pairs: Vec<(&str, &str)> = store.all_memberships().collect();
724 out.extend_from_slice(
725 &u32::try_from(pairs.len())
726 .expect("≤ 4G memberships")
727 .to_le_bytes(),
728 );
729 for (member, role) in pairs {
730 let ml = u16::try_from(member.len()).expect("≤ 65k name");
731 out.extend_from_slice(&ml.to_le_bytes());
732 out.extend_from_slice(member.as_bytes());
733 let rl = u16::try_from(role.len()).expect("≤ 65k name");
734 out.extend_from_slice(&rl.to_le_bytes());
735 out.extend_from_slice(role.as_bytes());
736 }
737 out
738}
739
740#[derive(Debug)]
741pub enum UserDeserializeError {
742 Truncated,
743 BadRole(u8),
744 InvalidUtf8,
745}
746
747impl core::fmt::Display for UserDeserializeError {
748 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
749 match self {
750 Self::Truncated => f.write_str("user blob truncated"),
751 Self::BadRole(b) => write!(f, "unknown role byte: {b}"),
752 Self::InvalidUtf8 => f.write_str("username not valid UTF-8"),
753 }
754 }
755}
756
757fn take<'a>(p: &mut usize, n: usize, buf: &'a [u8]) -> Result<&'a [u8], UserDeserializeError> {
758 if *p + n > buf.len() {
759 return Err(UserDeserializeError::Truncated);
760 }
761 let s = &buf[*p..*p + n];
762 *p += n;
763 Ok(s)
764}
765
766pub(crate) fn deserialize_users(buf: &[u8]) -> Result<UserStore, UserDeserializeError> {
767 let mut p = 0usize;
768 let (
781 scram_present_inline,
782 mysql_native_present_inline,
783 caching_sha2_present_inline,
784 role_attrs_inline,
785 password_declared_inline,
786 ) = if !buf.is_empty() && buf[0] == PASSWORD_DECLARED_FORMAT_MARKER {
787 p += 1;
788 (true, true, true, true, true)
789 } else if !buf.is_empty() && buf[0] == ROLE_ATTRS_FORMAT_MARKER {
790 p += 1;
791 (true, true, true, true, false)
792 } else if !buf.is_empty() && buf[0] == CACHING_SHA2_FORMAT_MARKER {
793 p += 1;
794 (true, true, true, false, false)
795 } else if !buf.is_empty() && buf[0] == MYSQL_NATIVE_FORMAT_MARKER {
796 p += 1;
797 (true, true, false, false, false)
798 } else if !buf.is_empty() && buf[0] == SCRAM_FORMAT_MARKER {
799 p += 1;
800 (true, false, false, false, false)
801 } else {
802 (false, false, false, false, false)
803 };
804 let count_bytes = take(&mut p, 4, buf)?;
805 let count = u32::from_le_bytes(count_bytes.try_into().unwrap()) as usize;
806 let mut store = UserStore::new();
807 for _ in 0..count {
808 let nl_bytes = take(&mut p, 2, buf)?;
809 let nl = u16::from_le_bytes(nl_bytes.try_into().unwrap()) as usize;
810 let name_bytes = take(&mut p, nl, buf)?;
811 let name = core::str::from_utf8(name_bytes)
812 .map_err(|_| UserDeserializeError::InvalidUtf8)?
813 .to_string();
814 let role_byte = take(&mut p, 1, buf)?[0];
815 let role = match role_byte {
816 0 => Role::Admin,
817 1 => Role::ReadWrite,
818 2 => Role::ReadOnly,
819 b => return Err(UserDeserializeError::BadRole(b)),
820 };
821 let mut salt = [0u8; SALT_LEN];
822 salt.copy_from_slice(take(&mut p, SALT_LEN, buf)?);
823 let mut hash = [0u8; HASH_LEN];
824 hash.copy_from_slice(take(&mut p, HASH_LEN, buf)?);
825 let scram = if scram_present_inline {
826 let flag = take(&mut p, 1, buf)?[0];
827 if flag == 1 {
828 let iters_bytes = take(&mut p, 4, buf)?;
829 let iters = u32::from_le_bytes(iters_bytes.try_into().unwrap());
830 let mut s_salt = [0u8; SCRAM_SALT_LEN];
831 s_salt.copy_from_slice(take(&mut p, SCRAM_SALT_LEN, buf)?);
832 let mut stored_key = [0u8; HASH_LEN];
833 stored_key.copy_from_slice(take(&mut p, HASH_LEN, buf)?);
834 let mut server_key = [0u8; HASH_LEN];
835 server_key.copy_from_slice(take(&mut p, HASH_LEN, buf)?);
836 Some(ScramSecrets {
837 iters,
838 salt: s_salt,
839 stored_key,
840 server_key,
841 })
842 } else {
843 None
844 }
845 } else {
846 None
847 };
848 let mysql_native = if mysql_native_present_inline {
849 let flag = take(&mut p, 1, buf)?[0];
850 if flag == 1 {
851 let mut h = [0u8; MYSQL_NATIVE_HASH_LEN];
852 h.copy_from_slice(take(&mut p, MYSQL_NATIVE_HASH_LEN, buf)?);
853 Some(h)
854 } else {
855 None
856 }
857 } else {
858 None
859 };
860 let caching_sha2 = if caching_sha2_present_inline {
861 let flag = take(&mut p, 1, buf)?[0];
862 if flag == 1 {
863 let mut h = [0u8; CACHING_SHA2_HASH_LEN];
864 h.copy_from_slice(take(&mut p, CACHING_SHA2_HASH_LEN, buf)?);
865 Some(h)
866 } else {
867 None
868 }
869 } else {
870 None
871 };
872 let (can_login, inherit, superuser) = if role_attrs_inline {
876 let b = take(&mut p, 3, buf)?;
877 (b[0] == 1, b[1] == 1, b[2] == 1)
878 } else {
879 (true, true, matches!(role, Role::Admin))
880 };
881 let password_declared = if password_declared_inline {
887 take(&mut p, 1, buf)?[0] == 1
888 } else {
889 can_login
890 };
891 store.users.insert(
892 name,
893 UserRecord {
894 role,
895 salt,
896 hash,
897 scram,
898 mysql_native,
899 caching_sha2,
900 can_login,
901 password_declared,
902 inherit,
903 superuser,
904 },
905 );
906 }
907 if role_attrs_inline {
908 let count_bytes = take(&mut p, 4, buf)?;
909 let mcount = u32::from_le_bytes(count_bytes.try_into().unwrap()) as usize;
910 for _ in 0..mcount {
911 let ml = u16::from_le_bytes(take(&mut p, 2, buf)?.try_into().unwrap()) as usize;
912 let member = core::str::from_utf8(take(&mut p, ml, buf)?)
913 .map_err(|_| UserDeserializeError::InvalidUtf8)?
914 .to_string();
915 let rl = u16::from_le_bytes(take(&mut p, 2, buf)?.try_into().unwrap()) as usize;
916 let role = core::str::from_utf8(take(&mut p, rl, buf)?)
917 .map_err(|_| UserDeserializeError::InvalidUtf8)?
918 .to_string();
919 store.add_member(&role, &member);
920 }
921 }
922 if p != buf.len() {
923 return Err(UserDeserializeError::Truncated);
924 }
925 Ok(store)
926}
927
928impl Engine {
929 pub(crate) fn exec_show_users(&self) -> QueryResult {
931 let columns = alloc::vec![
932 ColumnSchema::new("name", DataType::Text, false),
933 ColumnSchema::new("role", DataType::Text, false),
934 ];
935 let rows: Vec<Row<'static>> = self
936 .users
937 .iter()
938 .map(|(name, rec)| {
939 Row::new(alloc::vec![
940 Value::text(name.to_string()),
941 Value::text(rec.role.as_str().to_string()),
942 ])
943 })
944 .collect();
945 QueryResult::Rows { columns, rows }
946 }
947 pub fn create_user(
951 &mut self,
952 name: &str,
953 password: &str,
954 role: Role,
955 salt: [u8; 16],
956 ) -> Result<(), UserError> {
957 self.role_ddl_users_mut()
961 .create(name, password, role, salt)?;
962 let scram_salt = self.salt_fn.map_or_else(
968 || {
969 let mut s = [0u8; SCRAM_SALT_LEN];
970 let digest = spg_crypto::hash(name.as_bytes());
971 s.copy_from_slice(&digest[16..32]);
974 s
975 },
976 |f| f(),
977 );
978 self.role_ddl_users_mut()
979 .enable_scram(name, password, scram_salt, SCRAM_DEFAULT_ITERS)?;
980 Ok(())
981 }
982
983 pub fn drop_user(&mut self, name: &str) -> Result<(), UserError> {
984 self.role_ddl_users_mut().drop(name)
985 }
986
987 pub fn alter_user_password(
992 &mut self,
993 name: &str,
994 password: Option<&str>,
995 ) -> Result<(), UserError> {
996 let salt = self.salt_fn.map_or_else(
997 || {
998 let mut s_bytes = [0u8; 16];
999 let digest = spg_crypto::hash(name.as_bytes());
1000 s_bytes.copy_from_slice(&digest[..16]);
1001 s_bytes
1002 },
1003 |f| f(),
1004 );
1005 self.role_ddl_users_mut()
1006 .set_password(name, password, salt)?;
1007 if let Some(p) = password {
1008 let scram_salt = self.salt_fn.map_or_else(
1009 || {
1010 let mut s = [0u8; SCRAM_SALT_LEN];
1011 let digest = spg_crypto::hash(name.as_bytes());
1012 s.copy_from_slice(&digest[16..32]);
1013 s
1014 },
1015 |f| f(),
1016 );
1017 self.role_ddl_users_mut()
1018 .enable_scram(name, p, scram_salt, SCRAM_DEFAULT_ITERS)?;
1019 }
1020 Ok(())
1021 }
1022
1023 pub fn verify_user(&self, name: &str, password: &str) -> Option<Role> {
1024 self.users.verify(name, password)
1025 }
1026
1027 #[must_use]
1030 pub fn user_scram(&self, name: &str) -> Option<()> {
1031 self.users.get(name).and_then(|r| r.scram().map(|_| ()))
1032 }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 #[test]
1044 fn scram_secrets_match_live_pg_vector() {
1045 let salt: [u8; SCRAM_SALT_LEN] = [
1046 0xbb, 0x70, 0xc5, 0x3e, 0x8d, 0x2b, 0x56, 0x64, 0x12, 0xc0, 0xae, 0xd1, 0x4e, 0x19,
1047 0x8b, 0xfe,
1048 ];
1049 let want_stored: [u8; HASH_LEN] = [
1050 0xbb, 0x80, 0x16, 0x91, 0x1b, 0xc4, 0x49, 0x6f, 0x9d, 0x95, 0x79, 0xcd, 0xb8, 0x57,
1051 0x12, 0x01, 0x58, 0x2b, 0x52, 0x9a, 0x9e, 0x80, 0xe8, 0x06, 0x32, 0x3e, 0x76, 0x5f,
1052 0x38, 0xd1, 0x51, 0xa9,
1053 ];
1054 let want_server: [u8; HASH_LEN] = [
1055 0x21, 0xe4, 0x04, 0x40, 0x68, 0x5f, 0x80, 0x5c, 0x6d, 0x52, 0xc0, 0x47, 0x4d, 0xa3,
1056 0x5b, 0x96, 0xc0, 0x61, 0x10, 0x25, 0x2c, 0xf3, 0x31, 0x30, 0x00, 0x88, 0x5b, 0x08,
1057 0x8c, 0xe3, 0x0b, 0x84,
1058 ];
1059 let secrets = compute_scram_secrets("secret", salt, 4096);
1060 assert_eq!(secrets.iters, 4096);
1061 assert_eq!(secrets.salt, salt);
1062 assert_eq!(
1063 secrets.stored_key, want_stored,
1064 "StoredKey diverges from PG"
1065 );
1066 assert_eq!(
1067 secrets.server_key, want_server,
1068 "ServerKey diverges from PG"
1069 );
1070 }
1071
1072 #[test]
1077 fn sql_create_user_derives_scram() {
1078 let mut e = crate::Engine::new();
1079 e.execute("CREATE USER app WITH PASSWORD 'pw' ROLE 'readwrite'")
1080 .expect("CREATE USER");
1081 let rec = e.users.get("app").expect("user created");
1082 assert!(
1083 rec.scram().is_some(),
1084 "SQL CREATE USER must derive a SCRAM-SHA-256 verifier"
1085 );
1086 }
1087
1088 #[test]
1089 fn create_then_verify_succeeds_with_right_password_only() {
1090 let mut s = UserStore::new();
1091 s.create("alice", "hunter2", Role::Admin, [1; SALT_LEN])
1092 .unwrap();
1093 assert_eq!(s.verify("alice", "hunter2"), Some(Role::Admin));
1094 assert_eq!(s.verify("alice", "wrong"), None);
1095 assert_eq!(s.verify("bob", "hunter2"), None);
1096 }
1097
1098 #[test]
1099 fn create_duplicate_user_is_rejected() {
1100 let mut s = UserStore::new();
1101 s.create("a", "p", Role::ReadOnly, [0; SALT_LEN]).unwrap();
1102 assert_eq!(
1103 s.create("a", "p2", Role::Admin, [0; SALT_LEN]),
1104 Err(UserError::Exists)
1105 );
1106 }
1107
1108 #[test]
1109 fn drop_user_removes_them() {
1110 let mut s = UserStore::new();
1111 s.create("a", "p", Role::Admin, [0; SALT_LEN]).unwrap();
1112 s.drop("a").unwrap();
1113 assert!(s.is_empty());
1114 assert_eq!(s.drop("a"), Err(UserError::NotFound));
1115 }
1116
1117 #[test]
1118 fn role_parse_accepts_aliases() {
1119 assert_eq!(Role::parse("ADMIN"), Some(Role::Admin));
1120 assert_eq!(Role::parse("rw"), Some(Role::ReadWrite));
1121 assert_eq!(Role::parse("ro"), Some(Role::ReadOnly));
1122 assert_eq!(Role::parse("god"), None);
1123 }
1124
1125 #[test]
1126 fn snapshot_round_trip_preserves_users_and_verify() {
1127 let mut s = UserStore::new();
1128 s.create("alice", "pw1", Role::Admin, [7; SALT_LEN])
1129 .unwrap();
1130 s.create("bob", "pw2", Role::ReadOnly, [13; SALT_LEN])
1131 .unwrap();
1132 let bytes = serialize_users(&s);
1133 let s2 = deserialize_users(&bytes).unwrap();
1134 assert_eq!(s2.len(), 2);
1135 assert_eq!(s2.verify("alice", "pw1"), Some(Role::Admin));
1136 assert_eq!(s2.verify("bob", "pw2"), Some(Role::ReadOnly));
1137 assert_eq!(s2.verify("bob", "wrong"), None);
1138 }
1139
1140 #[test]
1141 fn empty_store_round_trip() {
1142 let s = UserStore::new();
1149 let bytes = serialize_users(&s);
1150 assert_eq!(bytes, [0xfb, 0, 0, 0, 0, 0, 0, 0, 0]);
1151 let s2 = deserialize_users(&bytes).unwrap();
1152 assert!(s2.is_empty());
1153 }
1154
1155 #[test]
1156 fn v2_blob_still_loads_with_mysql_native_none() {
1157 let mut buf = Vec::new();
1162 buf.push(0xff); buf.extend_from_slice(&1u32.to_le_bytes());
1164 buf.extend_from_slice(&3u16.to_le_bytes());
1165 buf.extend_from_slice(b"old");
1166 buf.push(0); buf.extend_from_slice(&[1u8; SALT_LEN]);
1168 buf.extend_from_slice(&[2u8; HASH_LEN]);
1169 buf.push(0); let s = deserialize_users(&buf).unwrap();
1171 let rec = s.get("old").expect("v2 user loads");
1172 assert!(rec.mysql_native().is_none());
1173 }
1174}
1175
1176#[cfg(test)]
1177mod p0_71_tests {
1178 use super::*;
1179
1180 #[test]
1181 fn create_populates_mysql_native_hash() {
1182 let mut s = UserStore::new();
1183 s.create("alice", "wonderland", Role::Admin, [9u8; SALT_LEN])
1184 .unwrap();
1185 let rec = s.get("alice").unwrap();
1186 let expected = compute_mysql_native_hash("wonderland");
1187 assert_eq!(rec.mysql_native(), Some(&expected));
1188 }
1189
1190 #[test]
1191 fn verify_mysql_native_password_accepts_correct_response() {
1192 let mut s = UserStore::new();
1194 s.create("bob", "secret", Role::Admin, [3u8; SALT_LEN])
1195 .unwrap();
1196 let rec = s.get("bob").unwrap();
1197 let scramble: [u8; 20] = core::array::from_fn(|i| (i as u8).wrapping_mul(7));
1198 let sha1_pwd = sha1_bytes(b"secret");
1200 let sha1_sha1_pwd = sha1_bytes(&sha1_pwd);
1201 let mut concat = [0u8; 40];
1202 concat[..20].copy_from_slice(&scramble);
1203 concat[20..].copy_from_slice(&sha1_sha1_pwd);
1204 let mask = sha1_bytes(&concat);
1205 let response: [u8; MYSQL_NATIVE_HASH_LEN] = core::array::from_fn(|i| sha1_pwd[i] ^ mask[i]);
1206 assert!(rec.verify_mysql_native_password(&scramble, &response));
1207 let mut bad = response;
1209 bad[0] ^= 1;
1210 assert!(!rec.verify_mysql_native_password(&scramble, &bad));
1211 }
1212
1213 #[test]
1214 fn v4_serialise_round_trips_both_mysql_native_and_caching_sha2() {
1215 let mut s = UserStore::new();
1216 s.create("alice", "wonderland", Role::Admin, [4u8; SALT_LEN])
1217 .unwrap();
1218 let bytes = serialize_users(&s);
1219 assert_eq!(bytes[0], 0xfb, "v6 marker advertised (round 548)");
1220 let s2 = deserialize_users(&bytes).unwrap();
1221 let r1 = s.get("alice").unwrap();
1222 let r2 = s2.get("alice").unwrap();
1223 assert_eq!(r1.mysql_native(), r2.mysql_native());
1224 assert_eq!(r1.caching_sha2(), r2.caching_sha2());
1225 assert!(r1.mysql_native().is_some());
1227 assert!(r1.caching_sha2().is_some());
1228 }
1229
1230 #[test]
1231 fn v3_blob_still_loads_with_caching_sha2_none() {
1232 let mut buf = Vec::new();
1236 buf.push(0xfe); buf.extend_from_slice(&1u32.to_le_bytes());
1238 buf.extend_from_slice(&5u16.to_le_bytes());
1239 buf.extend_from_slice(b"older");
1240 buf.push(0); buf.extend_from_slice(&[1u8; SALT_LEN]);
1242 buf.extend_from_slice(&[2u8; HASH_LEN]);
1243 buf.push(0); buf.push(1); buf.extend_from_slice(&[3u8; MYSQL_NATIVE_HASH_LEN]);
1246 let s = deserialize_users(&buf).unwrap();
1247 let rec = s.get("older").unwrap();
1248 assert!(rec.mysql_native().is_some());
1249 assert!(rec.caching_sha2().is_none());
1250 }
1251
1252 #[test]
1253 fn verify_caching_sha2_password_accepts_correct_response() {
1254 let mut s = UserStore::new();
1255 s.create("bob", "secret", Role::Admin, [3u8; SALT_LEN])
1256 .unwrap();
1257 let rec = s.get("bob").unwrap();
1258 let scramble: [u8; 20] = core::array::from_fn(|i| (i as u8).wrapping_mul(11));
1259 let sha_pwd = sha256_bytes(b"secret");
1260 let sha_sha_pwd = sha256_bytes(&sha_pwd);
1261 let mut concat = [0u8; 20 + CACHING_SHA2_HASH_LEN];
1262 concat[..20].copy_from_slice(&scramble);
1263 concat[20..].copy_from_slice(&sha_sha_pwd);
1264 let mask = sha256_bytes(&concat);
1265 let response: [u8; CACHING_SHA2_HASH_LEN] = core::array::from_fn(|i| sha_pwd[i] ^ mask[i]);
1266 assert!(rec.verify_caching_sha2_password(&scramble, &response));
1267 let mut bad = response;
1268 bad[0] ^= 1;
1269 assert!(!rec.verify_caching_sha2_password(&scramble, &bad));
1270 }
1271
1272 #[test]
1273 fn old_v1_user_blob_still_loads() {
1274 let mut buf = Vec::new();
1277 buf.extend_from_slice(&1u32.to_le_bytes());
1278 buf.extend_from_slice(&3u16.to_le_bytes());
1279 buf.extend_from_slice(b"bob");
1280 buf.push(0); buf.extend_from_slice(&[7u8; SALT_LEN]);
1282 buf.extend_from_slice(&[42u8; HASH_LEN]);
1283 let s = deserialize_users(&buf).expect("v1 blob must still load");
1284 assert_eq!(s.len(), 1);
1285 let (n, rec) = s.iter().next().unwrap();
1286 assert_eq!(n, "bob");
1287 assert_eq!(rec.role, Role::Admin);
1288 assert!(rec.scram().is_none(), "v1 users have no SCRAM secrets");
1289 }
1290
1291 #[test]
1292 fn scram_round_trip_preserves_iters_salt_keys() {
1293 let mut s = UserStore::new();
1294 s.create("alice", "pw", Role::Admin, [3; SALT_LEN]).unwrap();
1295 s.enable_scram("alice", "pw", [9; SCRAM_SALT_LEN], 4096)
1296 .unwrap();
1297 let bytes = serialize_users(&s);
1298 let s2 = deserialize_users(&bytes).unwrap();
1299 let (_, rec) = s2.iter().next().unwrap();
1300 let scram = rec.scram().expect("scram must round-trip");
1301 assert_eq!(scram.iters, 4096);
1302 assert_eq!(scram.salt, [9u8; SCRAM_SALT_LEN]);
1303 let expected = compute_scram_secrets("pw", [9; SCRAM_SALT_LEN], 4096);
1306 assert_eq!(scram.stored_key, expected.stored_key);
1307 assert_eq!(scram.server_key, expected.server_key);
1308 }
1309
1310 #[test]
1311 fn deserialize_truncation_is_caught() {
1312 assert!(deserialize_users(&[]).is_err());
1313 assert!(deserialize_users(&[0, 0, 0]).is_err());
1314 }
1315}