1use crate::AuthBackend;
8use anyhow::{anyhow, Context, Result};
9use async_trait::async_trait;
10use md5::Md5;
11use rusmes_proto::Username;
12use sha2::{Digest, Sha256, Sha512};
13use std::path::{Path, PathBuf};
14
15const CRYPT_B64: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
18
19fn b64_encode_bits(value: u32, n: usize) -> String {
21 let mut out = String::with_capacity(n);
22 let mut v = value;
23 for _ in 0..n {
24 out.push(CRYPT_B64[(v & 0x3f) as usize] as char);
25 v >>= 6;
26 }
27 out
28}
29
30const SHA512_REORDER: [(usize, usize, usize); 21] = [
37 (0, 21, 42),
38 (22, 43, 1),
39 (44, 2, 23),
40 (3, 24, 45),
41 (25, 46, 4),
42 (47, 5, 26),
43 (6, 27, 48),
44 (28, 49, 7),
45 (50, 8, 29),
46 (9, 30, 51),
47 (31, 52, 10),
48 (53, 11, 32),
49 (12, 33, 54),
50 (34, 55, 13),
51 (56, 14, 35),
52 (15, 36, 57),
53 (37, 58, 16),
54 (59, 17, 38),
55 (18, 39, 60),
56 (40, 61, 19),
57 (62, 20, 41),
58];
59
60fn sha512_crypt(password: &[u8], salt: &[u8], rounds: u32) -> String {
64 let p_len = password.len();
65 let s_len = salt.len();
66
67 let digest_b = {
69 let mut ctx = Sha512::new();
70 ctx.update(password);
71 ctx.update(salt);
72 ctx.update(password);
73 ctx.finalize()
74 };
75
76 let digest_a = {
78 let mut ctx = Sha512::new();
79 ctx.update(password); ctx.update(salt); let mut remaining = p_len;
84 while remaining >= 64 {
85 ctx.update(&digest_b[..]);
86 remaining -= 64;
87 }
88 if remaining > 0 {
89 ctx.update(&digest_b[..remaining]);
90 }
91
92 let mut n = p_len;
94 while n > 0 {
95 if n & 1 != 0 {
96 ctx.update(&digest_b[..]);
97 } else {
98 ctx.update(password);
99 }
100 n >>= 1;
101 }
102
103 ctx.finalize()
104 };
105
106 let p_bytes = {
108 let mut ctx = Sha512::new();
109 for _ in 0..p_len {
110 ctx.update(password);
111 }
112 let dp = ctx.finalize();
113
114 let mut buf = vec![0u8; p_len];
115 let mut off = 0;
116 while off + 64 <= p_len {
117 buf[off..off + 64].copy_from_slice(&dp[..]);
118 off += 64;
119 }
120 if off < p_len {
121 buf[off..].copy_from_slice(&dp[..p_len - off]);
122 }
123 buf
124 };
125
126 let s_bytes = {
128 let repeat_count = 16 + (digest_a[0] as usize);
129 let mut ctx = Sha512::new();
130 for _ in 0..repeat_count {
131 ctx.update(salt);
132 }
133 let ds = ctx.finalize();
134
135 let mut buf = vec![0u8; s_len];
136 let mut off = 0;
137 while off + 64 <= s_len {
138 buf[off..off + 64].copy_from_slice(&ds[..]);
139 off += 64;
140 }
141 if off < s_len {
142 buf[off..].copy_from_slice(&ds[..s_len - off]);
143 }
144 buf
145 };
146
147 let mut prev: [u8; 64] = digest_a.into();
149 for i in 0..rounds {
150 let mut ctx = Sha512::new();
151 if i & 1 != 0 {
152 ctx.update(&p_bytes);
153 } else {
154 ctx.update(prev);
155 }
156 if i % 3 != 0 {
157 ctx.update(&s_bytes);
158 }
159 if i % 7 != 0 {
160 ctx.update(&p_bytes);
161 }
162 if i & 1 != 0 {
163 ctx.update(prev);
164 } else {
165 ctx.update(&p_bytes);
166 }
167 let out = ctx.finalize();
168 prev.copy_from_slice(&out);
169 }
170
171 let mut encoded = String::with_capacity(86);
173 for &(a, b, c) in &SHA512_REORDER {
174 let v = (prev[a] as u32) << 16 | (prev[b] as u32) << 8 | (prev[c] as u32);
175 encoded.push_str(&b64_encode_bits(v, 4));
176 }
177 encoded.push_str(&b64_encode_bits(prev[63] as u32, 2));
179
180 encoded
181}
182
183fn sha512_crypt_full(password: &[u8], raw_salt: &str) -> String {
185 let (rounds, salt) = parse_rounds_and_salt(raw_salt);
186 let salt = if salt.len() > 16 { &salt[..16] } else { salt };
188 let hash = sha512_crypt(password, salt.as_bytes(), rounds);
189 if rounds == 5000 {
190 format!("$6${salt}${hash}")
191 } else {
192 format!("$6$rounds={rounds}${salt}${hash}")
193 }
194}
195
196const SHA256_REORDER: [(usize, usize, usize); 10] = [
201 (0, 10, 20),
202 (21, 1, 11),
203 (12, 22, 2),
204 (3, 13, 23),
205 (24, 4, 14),
206 (15, 25, 5),
207 (6, 16, 26),
208 (27, 7, 17),
209 (18, 28, 8),
210 (9, 19, 29),
211];
212
213fn sha256_crypt(password: &[u8], salt: &[u8], rounds: u32) -> String {
215 let p_len = password.len();
216 let s_len = salt.len();
217
218 let digest_b = {
220 let mut ctx = Sha256::new();
221 ctx.update(password);
222 ctx.update(salt);
223 ctx.update(password);
224 ctx.finalize()
225 };
226
227 let digest_a = {
229 let mut ctx = Sha256::new();
230 ctx.update(password);
231 ctx.update(salt);
232
233 let mut remaining = p_len;
234 while remaining >= 32 {
235 ctx.update(&digest_b[..]);
236 remaining -= 32;
237 }
238 if remaining > 0 {
239 ctx.update(&digest_b[..remaining]);
240 }
241
242 let mut n = p_len;
243 while n > 0 {
244 if n & 1 != 0 {
245 ctx.update(&digest_b[..]);
246 } else {
247 ctx.update(password);
248 }
249 n >>= 1;
250 }
251
252 ctx.finalize()
253 };
254
255 let p_bytes = {
257 let mut ctx = Sha256::new();
258 for _ in 0..p_len {
259 ctx.update(password);
260 }
261 let dp = ctx.finalize();
262
263 let mut buf = vec![0u8; p_len];
264 let mut off = 0;
265 while off + 32 <= p_len {
266 buf[off..off + 32].copy_from_slice(&dp[..]);
267 off += 32;
268 }
269 if off < p_len {
270 buf[off..].copy_from_slice(&dp[..p_len - off]);
271 }
272 buf
273 };
274
275 let s_bytes = {
277 let repeat_count = 16 + (digest_a[0] as usize);
278 let mut ctx = Sha256::new();
279 for _ in 0..repeat_count {
280 ctx.update(salt);
281 }
282 let ds = ctx.finalize();
283
284 let mut buf = vec![0u8; s_len];
285 let mut off = 0;
286 while off + 32 <= s_len {
287 buf[off..off + 32].copy_from_slice(&ds[..]);
288 off += 32;
289 }
290 if off < s_len {
291 buf[off..].copy_from_slice(&ds[..s_len - off]);
292 }
293 buf
294 };
295
296 let mut prev: [u8; 32] = digest_a.into();
298 for i in 0..rounds {
299 let mut ctx = Sha256::new();
300 if i & 1 != 0 {
301 ctx.update(&p_bytes);
302 } else {
303 ctx.update(prev);
304 }
305 if i % 3 != 0 {
306 ctx.update(&s_bytes);
307 }
308 if i % 7 != 0 {
309 ctx.update(&p_bytes);
310 }
311 if i & 1 != 0 {
312 ctx.update(prev);
313 } else {
314 ctx.update(&p_bytes);
315 }
316 let out = ctx.finalize();
317 prev.copy_from_slice(&out);
318 }
319
320 let mut encoded = String::with_capacity(43);
322 for &(a, b, c) in &SHA256_REORDER {
323 let v = (prev[a] as u32) << 16 | (prev[b] as u32) << 8 | (prev[c] as u32);
324 encoded.push_str(&b64_encode_bits(v, 4));
325 }
326 let v = (prev[31] as u32) << 8 | (prev[30] as u32);
328 encoded.push_str(&b64_encode_bits(v, 3));
329
330 encoded
331}
332
333fn sha256_crypt_full(password: &[u8], raw_salt: &str) -> String {
335 let (rounds, salt) = parse_rounds_and_salt(raw_salt);
336 let salt = if salt.len() > 16 { &salt[..16] } else { salt };
337 let hash = sha256_crypt(password, salt.as_bytes(), rounds);
338 if rounds == 5000 {
339 format!("$5${salt}${hash}")
340 } else {
341 format!("$5$rounds={rounds}${salt}${hash}")
342 }
343}
344
345fn md5_crypt(password: &[u8], salt: &[u8]) -> String {
349 let p_len = password.len();
350
351 let digest_b = {
353 let mut ctx = Md5::new();
354 ctx.update(password);
355 ctx.update(salt);
356 ctx.update(password);
357 ctx.finalize()
358 };
359
360 let mut ctx = Md5::new();
362 ctx.update(password);
363 ctx.update(b"$1$");
364 ctx.update(salt);
365
366 let mut remaining = p_len;
368 while remaining >= 16 {
369 ctx.update(&digest_b[..]);
370 remaining -= 16;
371 }
372 if remaining > 0 {
373 ctx.update(&digest_b[..remaining]);
374 }
375
376 let mut n = p_len;
378 while n > 0 {
379 if n & 1 != 0 {
380 ctx.update([0u8]);
381 } else {
382 ctx.update(&password[..1]);
383 }
384 n >>= 1;
385 }
386
387 let mut result: [u8; 16] = ctx.finalize().into();
388
389 for i in 0..1000u32 {
391 let mut ctx2 = Md5::new();
392 if i & 1 != 0 {
393 ctx2.update(password);
394 } else {
395 ctx2.update(result);
396 }
397 if i % 3 != 0 {
398 ctx2.update(salt);
399 }
400 if i % 7 != 0 {
401 ctx2.update(password);
402 }
403 if i & 1 != 0 {
404 ctx2.update(result);
405 } else {
406 ctx2.update(password);
407 }
408 result = ctx2.finalize().into();
409 }
410
411 let mut encoded = String::with_capacity(22);
413 let groups: [(usize, usize, usize); 5] =
414 [(0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)];
415 for (a, b, c) in groups {
416 let v = (result[a] as u32) << 16 | (result[b] as u32) << 8 | (result[c] as u32);
417 encoded.push_str(&b64_encode_bits(v, 4));
418 }
419 encoded.push_str(&b64_encode_bits(result[11] as u32, 2));
421
422 encoded
423}
424
425fn md5_crypt_full(password: &[u8], salt: &str) -> String {
426 let hash = md5_crypt(password, salt.as_bytes());
427 format!("$1${salt}${hash}")
428}
429
430fn parse_rounds_and_salt(raw: &str) -> (u32, &str) {
438 if let Some(rest) = raw.strip_prefix("rounds=") {
439 if let Some(dollar_pos) = rest.find('$') {
440 let rounds_str = &rest[..dollar_pos];
441 let salt = &rest[dollar_pos + 1..];
442 let rounds = rounds_str
443 .parse::<u32>()
444 .unwrap_or(5000)
445 .clamp(1000, 999_999_999);
446 return (rounds, salt);
447 }
448 }
449 (5000, raw)
450}
451
452fn verify_password(password: &str, hash: &str) -> Result<bool> {
460 if hash.starts_with("$6$") {
461 let inner = hash
462 .strip_prefix("$6$")
463 .ok_or_else(|| anyhow!("invalid $6$ hash"))?;
464 let last_dollar = inner
466 .rfind('$')
467 .ok_or_else(|| anyhow!("malformed $6$ hash: missing final delimiter"))?;
468 let raw_salt = &inner[..last_dollar];
469 let computed = sha512_crypt_full(password.as_bytes(), raw_salt);
470 Ok(computed == hash)
471 } else if hash.starts_with("$5$") {
472 let inner = hash
473 .strip_prefix("$5$")
474 .ok_or_else(|| anyhow!("invalid $5$ hash"))?;
475 let last_dollar = inner
476 .rfind('$')
477 .ok_or_else(|| anyhow!("malformed $5$ hash: missing final delimiter"))?;
478 let raw_salt = &inner[..last_dollar];
479 let computed = sha256_crypt_full(password.as_bytes(), raw_salt);
480 Ok(computed == hash)
481 } else if hash.starts_with("$1$") {
482 let inner = hash
483 .strip_prefix("$1$")
484 .ok_or_else(|| anyhow!("invalid $1$ hash"))?;
485 let last_dollar = inner
486 .rfind('$')
487 .ok_or_else(|| anyhow!("malformed $1$ hash: missing final delimiter"))?;
488 let salt = &inner[..last_dollar];
489 let computed = md5_crypt_full(password.as_bytes(), salt);
490 Ok(computed == hash)
491 } else if hash.starts_with("$2b$") || hash.starts_with("$2a$") || hash.starts_with("$2y$") {
492 bcrypt::verify(password, hash).map_err(|e| anyhow!("bcrypt verification error: {e}"))
493 } else if hash == "*" || hash == "!" || hash == "!!" || hash.starts_with("!") {
494 Ok(false)
496 } else {
497 Err(anyhow!(
498 "unsupported password hash scheme: {}",
499 &hash[..hash.len().min(10)]
500 ))
501 }
502}
503
504#[derive(Debug, Clone)]
508struct PasswdEntry {
509 username: String,
510 uid: u32,
511 #[allow(dead_code)]
512 gid: u32,
513 #[allow(dead_code)]
514 gecos: String,
515 #[allow(dead_code)]
516 home: String,
517 #[allow(dead_code)]
518 shell: String,
519}
520
521fn parse_passwd_line(line: &str) -> Option<PasswdEntry> {
524 let line = line.trim();
525 if line.is_empty() || line.starts_with('#') {
526 return None;
527 }
528 let parts: Vec<&str> = line.split(':').collect();
529 if parts.len() < 7 {
530 return None;
531 }
532 let uid = parts[2].parse::<u32>().ok()?;
533 let gid = parts[3].parse::<u32>().ok()?;
534 Some(PasswdEntry {
535 username: parts[0].to_owned(),
536 uid,
537 gid,
538 gecos: parts[4].to_owned(),
539 home: parts[5].to_owned(),
540 shell: parts[6].to_owned(),
541 })
542}
543
544async fn read_passwd_file(path: &Path) -> Result<Vec<PasswdEntry>> {
546 let content = tokio::fs::read_to_string(path)
547 .await
548 .with_context(|| format!("failed to read {}", path.display()))?;
549 Ok(content.lines().filter_map(parse_passwd_line).collect())
550}
551
552#[derive(Debug, Clone)]
556struct ShadowEntry {
557 username: String,
558 hash: String,
559 locked: bool,
561}
562
563fn parse_shadow_line(line: &str) -> Option<ShadowEntry> {
565 let line = line.trim();
566 if line.is_empty() || line.starts_with('#') {
567 return None;
568 }
569 let parts: Vec<&str> = line.splitn(9, ':').collect();
570 if parts.len() < 2 {
571 return None;
572 }
573 let username = parts[0].to_owned();
574 let raw_hash = parts[1];
575 let locked = raw_hash.starts_with('!') || raw_hash.starts_with('*');
576
577 let hash = if raw_hash.starts_with('!') && raw_hash.len() > 1 && raw_hash != "!!" {
579 raw_hash[1..].to_owned()
580 } else {
581 raw_hash.to_owned()
582 };
583
584 Some(ShadowEntry {
585 username,
586 hash,
587 locked,
588 })
589}
590
591async fn read_shadow_file(path: &Path) -> Result<Vec<ShadowEntry>> {
593 let content = tokio::fs::read_to_string(path)
594 .await
595 .with_context(|| format!("failed to read {}", path.display()))?;
596 Ok(content.lines().filter_map(parse_shadow_line).collect())
597}
598
599#[derive(Debug, Clone)]
603pub struct SystemConfig {
604 pub passwd_path: PathBuf,
606 pub shadow_path: PathBuf,
608 pub min_uid: u32,
610 pub allow_system_users: bool,
613}
614
615impl Default for SystemConfig {
616 fn default() -> Self {
617 Self {
618 passwd_path: PathBuf::from("/etc/passwd"),
619 shadow_path: PathBuf::from("/etc/shadow"),
620 min_uid: 1000,
621 allow_system_users: false,
622 }
623 }
624}
625
626pub struct SystemAuthBackend {
630 config: SystemConfig,
631}
632
633impl SystemAuthBackend {
634 pub fn new(config: SystemConfig) -> Self {
636 Self { config }
637 }
638
639 fn is_regular_uid(&self, uid: u32) -> bool {
641 self.config.allow_system_users || uid >= self.config.min_uid
642 }
643}
644
645#[async_trait]
646impl AuthBackend for SystemAuthBackend {
647 async fn authenticate(&self, username: &Username, password: &str) -> Result<bool> {
648 let entries = read_shadow_file(&self.config.shadow_path).await?;
649 let entry = entries.iter().find(|e| e.username == username.as_str());
650
651 let entry = match entry {
652 Some(e) => e,
653 None => return Ok(false),
654 };
655
656 if entry.locked {
657 return Ok(false);
658 }
659
660 let hash = entry.hash.clone();
661 let pw = password.to_owned();
662
663 tokio::task::spawn_blocking(move || verify_password(&pw, &hash))
665 .await
666 .map_err(|e| anyhow!("join error: {e}"))?
667 }
668
669 async fn verify_identity(&self, username: &Username) -> Result<bool> {
670 let entries = read_passwd_file(&self.config.passwd_path).await?;
671 Ok(entries
672 .iter()
673 .any(|e| e.username == username.as_str() && self.is_regular_uid(e.uid)))
674 }
675
676 async fn list_users(&self) -> Result<Vec<Username>> {
677 let entries = read_passwd_file(&self.config.passwd_path).await?;
678 let mut users = Vec::new();
679 for entry in &entries {
680 if !self.is_regular_uid(entry.uid) {
681 continue;
682 }
683 if let Ok(u) = Username::new(entry.username.clone()) {
684 users.push(u);
685 }
686 }
687 Ok(users)
688 }
689
690 async fn create_user(&self, _username: &Username, _password: &str) -> Result<()> {
691 Err(anyhow!(
692 "system backend is read-only; use useradd(8) to create system users"
693 ))
694 }
695
696 async fn delete_user(&self, _username: &Username) -> Result<()> {
697 Err(anyhow!(
698 "system backend is read-only; use userdel(8) to delete system users"
699 ))
700 }
701
702 async fn change_password(&self, _username: &Username, _new_password: &str) -> Result<()> {
703 Err(anyhow!(
704 "system backend is read-only; use passwd(1) to change passwords"
705 ))
706 }
707}
708
709#[cfg(test)]
714mod tests {
715 use super::*;
716 use std::io::Write;
717
718 #[test]
721 fn test_sha512_crypt_vector_1() {
722 let hash = sha512_crypt_full(b"Hello world!", "saltstring");
723 assert_eq!(
724 hash,
725 "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"
726 );
727 }
728
729 #[test]
730 fn test_sha512_crypt_vector_2() {
731 let hash = sha512_crypt_full(b"Hello world!", "rounds=10000$saltstringsaltstring");
732 assert_eq!(
733 hash,
734 "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."
735 );
736 }
737
738 #[test]
739 fn test_sha512_crypt_vector_3_rounds_5000() {
740 let hash = sha512_crypt_full(b"Hello world!", "rounds=5000$saltstring");
743 assert_eq!(
744 hash,
745 "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"
746 );
747 }
748
749 #[test]
750 fn test_sha512_crypt_vector_4_long_salt() {
751 let hash = sha512_crypt_full(
752 b"a]very.]long.]password",
753 "rounds=1400$anotherlongsaltstring",
754 );
755 assert_eq!(
756 hash,
757 "$6$rounds=1400$anotherlongsalts$Qfvpda9/GjV7Wb8GUTT6zacXCbXD87betTdwA7oey1xJInUU7wpEJ4J2WJ0UIrAePuYGKy86Do7Cdj.JxTpiN."
758 );
759 }
760
761 #[test]
762 fn test_sha512_crypt_vector_5_very_long_salt() {
763 let hash = sha512_crypt_full(
764 b"we have a short salt string but not a short password",
765 "rounds=77777$short",
766 );
767 assert_eq!(
768 hash,
769 "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0"
770 );
771 }
772
773 #[test]
774 fn test_sha512_crypt_vector_6_low_rounds() {
775 let hash = sha512_crypt_full(
777 b"the minimum number is still observed",
778 "rounds=1000$roundstoolow",
779 );
780 assert_eq!(
781 hash,
782 "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."
783 );
784 }
785
786 #[test]
789 fn test_sha256_crypt_vector_1() {
790 let hash = sha256_crypt_full(b"Hello world!", "saltstring");
791 assert_eq!(
792 hash,
793 "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5"
794 );
795 }
796
797 #[test]
798 fn test_sha256_crypt_vector_2() {
799 let hash = sha256_crypt_full(b"Hello world!", "rounds=10000$saltstringsaltstring");
800 assert_eq!(
801 hash,
802 "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA"
803 );
804 }
805
806 #[test]
807 fn test_sha256_crypt_vector_3() {
808 let hash = sha256_crypt_full(b"Hello world!", "rounds=5000$saltstring");
809 assert_eq!(
810 hash,
811 "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5"
812 );
813 }
814
815 #[test]
816 fn test_sha256_crypt_vector_4() {
817 let hash = sha256_crypt_full(
818 b"a]very.]long.]password",
819 "rounds=1400$anotherlongsaltstring",
820 );
821 assert_eq!(
822 hash,
823 "$5$rounds=1400$anotherlongsalts$8fc8RpnsAEYdbUkzdb0Tt9jps8e3xnDYAbqtN8Gmdl3"
824 );
825 }
826
827 #[test]
828 fn test_sha256_crypt_vector_5() {
829 let hash = sha256_crypt_full(
830 b"we have a short salt string but not a short password",
831 "rounds=77777$short",
832 );
833 assert_eq!(
834 hash,
835 "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/"
836 );
837 }
838
839 #[test]
840 fn test_sha256_crypt_vector_6() {
841 let hash = sha256_crypt_full(
842 b"the minimum number is still observed",
843 "rounds=1000$roundstoolow",
844 );
845 assert_eq!(
846 hash,
847 "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC"
848 );
849 }
850
851 #[test]
854 fn test_verify_sha512() {
855 let hash = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
856 assert!(verify_password("Hello world!", hash).expect("verify ok"));
857 assert!(!verify_password("wrong", hash).expect("verify ok"));
858 }
859
860 #[test]
861 fn test_verify_sha256() {
862 let hash = "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5";
863 assert!(verify_password("Hello world!", hash).expect("verify ok"));
864 assert!(!verify_password("wrong", hash).expect("verify ok"));
865 }
866
867 #[test]
868 fn test_verify_locked_account() {
869 assert!(!verify_password("anything", "!").expect("verify ok"));
870 assert!(!verify_password("anything", "*").expect("verify ok"));
871 assert!(!verify_password("anything", "!!").expect("verify ok"));
872 }
873
874 #[test]
877 fn test_parse_passwd_line_valid() {
878 let entry = parse_passwd_line("alice:x:1001:1001:Alice:/home/alice:/bin/bash");
879 let e = entry.expect("should parse");
880 assert_eq!(e.username, "alice");
881 assert_eq!(e.uid, 1001);
882 }
883
884 #[test]
885 fn test_parse_passwd_line_system_user() {
886 let entry = parse_passwd_line("daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin");
887 let e = entry.expect("should parse");
888 assert_eq!(e.username, "daemon");
889 assert_eq!(e.uid, 1);
890 }
891
892 #[test]
893 fn test_parse_passwd_line_comment() {
894 assert!(parse_passwd_line("# a comment").is_none());
895 }
896
897 #[test]
898 fn test_parse_passwd_line_empty() {
899 assert!(parse_passwd_line("").is_none());
900 assert!(parse_passwd_line(" ").is_none());
901 }
902
903 #[test]
904 fn test_parse_passwd_line_malformed() {
905 assert!(parse_passwd_line("no-colons-at-all").is_none());
906 assert!(parse_passwd_line("only:two:fields").is_none());
907 }
908
909 #[test]
912 fn test_parse_shadow_line_valid() {
913 let line = "alice:$6$salt$hash:19000:0:99999:7:::";
914 let e = parse_shadow_line(line).expect("should parse");
915 assert_eq!(e.username, "alice");
916 assert_eq!(e.hash, "$6$salt$hash");
917 assert!(!e.locked);
918 }
919
920 #[test]
921 fn test_parse_shadow_line_locked_bang() {
922 let line = "bob:!$6$salt$hash:19000:0:99999:7:::";
923 let e = parse_shadow_line(line).expect("should parse");
924 assert_eq!(e.username, "bob");
925 assert!(e.locked);
926 assert_eq!(e.hash, "$6$salt$hash");
928 }
929
930 #[test]
931 fn test_parse_shadow_line_locked_star() {
932 let line = "nologin:*:19000:0:99999:7:::";
933 let e = parse_shadow_line(line).expect("should parse");
934 assert!(e.locked);
935 }
936
937 #[test]
938 fn test_parse_shadow_line_locked_double_bang() {
939 let line = "newuser:!!:19000:0:99999:7:::";
940 let e = parse_shadow_line(line).expect("should parse");
941 assert!(e.locked);
942 assert_eq!(e.hash, "!!");
943 }
944
945 #[test]
946 fn test_parse_shadow_line_comment_and_empty() {
947 assert!(parse_shadow_line("# comment").is_none());
948 assert!(parse_shadow_line("").is_none());
949 }
950
951 #[test]
954 fn test_system_config_default() {
955 let cfg = SystemConfig::default();
956 assert_eq!(cfg.passwd_path, PathBuf::from("/etc/passwd"));
957 assert_eq!(cfg.shadow_path, PathBuf::from("/etc/shadow"));
958 assert_eq!(cfg.min_uid, 1000);
959 assert!(!cfg.allow_system_users);
960 }
961
962 #[test]
963 fn test_system_backend_custom_paths() {
964 let cfg = SystemConfig {
965 passwd_path: PathBuf::from("/tmp/test_passwd"),
966 shadow_path: PathBuf::from("/tmp/test_shadow"),
967 min_uid: 500,
968 allow_system_users: true,
969 };
970 let backend = SystemAuthBackend::new(cfg);
971 assert_eq!(
972 backend.config.passwd_path,
973 PathBuf::from("/tmp/test_passwd")
974 );
975 assert!(backend.config.allow_system_users);
976 }
977
978 fn write_temp_file(prefix: &str, content: &str) -> PathBuf {
980 let dir = std::env::temp_dir();
981 let ts = std::time::SystemTime::now()
982 .duration_since(std::time::UNIX_EPOCH)
983 .unwrap_or_default()
984 .as_nanos();
985 let path = dir.join(format!("rusmes_test_{}_{}", prefix, ts));
986 let mut f = std::fs::File::create(&path).expect("create temp file");
987 f.write_all(content.as_bytes()).expect("write temp file");
988 path
989 }
990
991 #[tokio::test]
992 async fn test_authenticate_sha512() {
993 let pw_hash = sha512_crypt_full(b"testpass", "testsalt");
995 let shadow_content = format!("alice:{}:19000:0:99999:7:::\n", pw_hash);
996 let shadow_path = write_temp_file("shadow_auth", &shadow_content);
997
998 let passwd_content = "alice:x:1001:1001:Alice:/home/alice:/bin/bash\n";
999 let passwd_path = write_temp_file("passwd_auth", passwd_content);
1000
1001 let backend = SystemAuthBackend::new(SystemConfig {
1002 passwd_path: passwd_path.clone(),
1003 shadow_path: shadow_path.clone(),
1004 ..SystemConfig::default()
1005 });
1006
1007 let user = Username::new("alice").expect("valid username");
1008 assert!(backend
1009 .authenticate(&user, "testpass")
1010 .await
1011 .expect("auth ok"));
1012 assert!(!backend.authenticate(&user, "wrong").await.expect("auth ok"));
1013
1014 let _ = std::fs::remove_file(&shadow_path);
1016 let _ = std::fs::remove_file(&passwd_path);
1017 }
1018
1019 #[tokio::test]
1020 async fn test_authenticate_locked_account() {
1021 let shadow_content = "bob:!$6$salt$fakehash:19000:0:99999:7:::\n";
1022 let shadow_path = write_temp_file("shadow_locked", shadow_content);
1023
1024 let backend = SystemAuthBackend::new(SystemConfig {
1025 shadow_path: shadow_path.clone(),
1026 ..SystemConfig::default()
1027 });
1028
1029 let user = Username::new("bob").expect("valid username");
1030 assert!(!backend
1031 .authenticate(&user, "anything")
1032 .await
1033 .expect("auth ok"));
1034
1035 let _ = std::fs::remove_file(&shadow_path);
1036 }
1037
1038 #[tokio::test]
1039 async fn test_verify_identity() {
1040 let content = "\
1041root:x:0:0:root:/root:/bin/bash
1042alice:x:1001:1001:Alice:/home/alice:/bin/bash
1043bob:x:1002:1002:Bob:/home/bob:/bin/bash
1044";
1045 let path = write_temp_file("passwd_verify", content);
1046
1047 let backend = SystemAuthBackend::new(SystemConfig {
1048 passwd_path: path.clone(),
1049 ..SystemConfig::default()
1050 });
1051
1052 let alice = Username::new("alice").expect("valid");
1053 let root = Username::new("root").expect("valid");
1054 let ghost = Username::new("ghost").expect("valid");
1055
1056 assert!(backend.verify_identity(&alice).await.expect("ok"));
1057 assert!(!backend.verify_identity(&root).await.expect("ok"));
1059 assert!(!backend.verify_identity(&ghost).await.expect("ok"));
1060
1061 let _ = std::fs::remove_file(&path);
1062 }
1063
1064 #[tokio::test]
1065 async fn test_list_users_filters_system() {
1066 let content = "\
1067root:x:0:0:root:/root:/bin/bash
1068daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
1069alice:x:1001:1001:Alice:/home/alice:/bin/bash
1070bob:x:1002:1002:Bob:/home/bob:/bin/bash
1071";
1072 let path = write_temp_file("passwd_list", content);
1073
1074 let backend = SystemAuthBackend::new(SystemConfig {
1075 passwd_path: path.clone(),
1076 ..SystemConfig::default()
1077 });
1078
1079 let users = backend.list_users().await.expect("ok");
1080 let names: Vec<String> = users.iter().map(|u| u.as_str().to_owned()).collect();
1081
1082 assert_eq!(names, vec!["alice", "bob"]);
1083
1084 let _ = std::fs::remove_file(&path);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_list_users_allow_system() {
1089 let content = "\
1090root:x:0:0:root:/root:/bin/bash
1091alice:x:1001:1001:Alice:/home/alice:/bin/bash
1092";
1093 let path = write_temp_file("passwd_allow_sys", content);
1094
1095 let backend = SystemAuthBackend::new(SystemConfig {
1096 passwd_path: path.clone(),
1097 allow_system_users: true,
1098 ..SystemConfig::default()
1099 });
1100
1101 let users = backend.list_users().await.expect("ok");
1102 assert_eq!(users.len(), 2);
1103
1104 let _ = std::fs::remove_file(&path);
1105 }
1106
1107 #[tokio::test]
1108 async fn test_create_delete_change_return_errors() {
1109 let backend = SystemAuthBackend::new(SystemConfig::default());
1110 let user = Username::new("test").expect("valid");
1111
1112 assert!(backend.create_user(&user, "pw").await.is_err());
1113 assert!(backend.delete_user(&user).await.is_err());
1114 assert!(backend.change_password(&user, "new").await.is_err());
1115 }
1116
1117 #[test]
1120 fn test_parse_rounds_and_salt_default() {
1121 let (rounds, salt) = parse_rounds_and_salt("mysalt");
1122 assert_eq!(rounds, 5000);
1123 assert_eq!(salt, "mysalt");
1124 }
1125
1126 #[test]
1127 fn test_parse_rounds_and_salt_explicit() {
1128 let (rounds, salt) = parse_rounds_and_salt("rounds=10000$mysalt");
1129 assert_eq!(rounds, 10000);
1130 assert_eq!(salt, "mysalt");
1131 }
1132
1133 #[test]
1134 fn test_parse_rounds_and_salt_clamped_low() {
1135 let (rounds, _) = parse_rounds_and_salt("rounds=100$mysalt");
1136 assert_eq!(rounds, 1000);
1137 }
1138
1139 #[test]
1142 fn test_md5_crypt_basic() {
1143 let hash = md5_crypt_full(b"password", "3edqd5Yh");
1149 assert!(hash.starts_with("$1$3edqd5Yh$"));
1150 assert!(verify_password("password", &hash).expect("verify ok"));
1151 }
1152
1153 #[test]
1156 fn test_b64_encode_bits_zero() {
1157 assert_eq!(b64_encode_bits(0, 4), "....");
1158 }
1159
1160 #[test]
1161 fn test_b64_encode_bits_single() {
1162 assert_eq!(b64_encode_bits(1, 1), "/");
1164 }
1165}