1#![no_std]
2#![doc = include_str!("../README.md")]
3#![warn(missing_docs)]
4#![warn(rust_2018_idioms)]
5#![warn(unreachable_pub)]
6#![deny(unsafe_op_in_unsafe_fn)]
7
8#[cfg(not(any(
20 feature = "backend-aws-lc",
21 feature = "backend-boring",
22 feature = "backend-openssl",
23 feature = "backend-rust-crypto",
24)))]
25compile_error!(
26 "crypt-sha512: no backend selected. Enable exactly one of the cargo features: \
27 `backend-aws-lc`, `backend-boring`, `backend-openssl`, `backend-rust-crypto`."
28);
29
30#[cfg(any(
31 all(feature = "backend-aws-lc", feature = "backend-boring"),
32 all(feature = "backend-aws-lc", feature = "backend-openssl"),
33 all(feature = "backend-aws-lc", feature = "backend-rust-crypto"),
34 all(feature = "backend-boring", feature = "backend-openssl"),
35 all(feature = "backend-boring", feature = "backend-rust-crypto"),
36 all(feature = "backend-openssl", feature = "backend-rust-crypto"),
37))]
38compile_error!(
39 "crypt-sha512: more than one backend selected. The `backend-*` features are \
40 mutually exclusive; enable exactly one of \
41 `backend-aws-lc`, `backend-boring`, `backend-openssl`, `backend-rust-crypto`."
42);
43
44extern crate alloc;
45
46use alloc::string::String;
47use alloc::vec::Vec;
48use core::fmt;
49
50mod backend;
51#[cfg(any(
55 feature = "backend-aws-lc",
56 feature = "backend-boring",
57 feature = "backend-openssl",
58 feature = "backend-rust-crypto",
59))]
60use crate::backend as crypto;
61#[cfg(any(
62 feature = "backend-aws-lc",
63 feature = "backend-boring",
64 feature = "backend-openssl",
65 feature = "backend-rust-crypto",
66))]
67use crate::backend::Sha512Context;
68
69const SHA512_SALT_PREFIX_STR: &str = "$6$";
70const SHA512_SALT_PREFIX: &[u8] = SHA512_SALT_PREFIX_STR.as_bytes();
71const SHA512_ROUNDS_PREFIX: &[u8] = b"rounds=";
72const SALT_LEN_MAX: usize = 16;
73const ROUNDS_DEFAULT: u32 = 5000;
74const ROUNDS_MIN: u32 = 1000;
75const ROUNDS_MAX: u32 = 999_999_999;
76
77const SHA512_HASH_ENCODED_LENGTH: usize = 86;
79
80const B64_CHARS: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
82
83pub struct Password {
103 bytes: Vec<u8>,
104}
105
106impl Password {
107 #[inline]
110 pub fn from_bytes(bytes: Vec<u8>) -> Self {
111 Self { bytes }
112 }
113
114 #[inline]
117 fn into_bytes(self) -> Vec<u8> {
118 let mut me = core::mem::ManuallyDrop::new(self);
120 core::mem::take(&mut me.bytes)
121 }
122}
123
124impl From<String> for Password {
125 #[inline]
129 fn from(s: String) -> Self {
130 Self {
131 bytes: s.into_bytes(),
132 }
133 }
134}
135
136impl From<&str> for Password {
137 #[inline]
142 fn from(s: &str) -> Self {
143 Self {
144 bytes: s.as_bytes().to_vec(),
145 }
146 }
147}
148
149impl From<Vec<u8>> for Password {
150 #[inline]
151 fn from(bytes: Vec<u8>) -> Self {
152 Self { bytes }
153 }
154}
155
156impl Drop for Password {
157 fn drop(&mut self) {
158 crypto::secure_zero_bytes(&mut self.bytes);
159 }
160}
161
162impl fmt::Debug for Password {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.debug_struct("Password").finish_non_exhaustive()
165 }
166}
167
168#[inline(always)]
171fn salt_spec_output_size(salt_len: usize, rounds_custom: bool) -> usize {
172 let mut size = SHA512_SALT_PREFIX.len() + salt_len + 1;
176
177 if rounds_custom {
182 size += SHA512_ROUNDS_PREFIX.len() + 9 + 1;
183 }
184 size
185}
186
187fn generate_salt(buf: &mut [u8]) {
190 crypto::random_bytes(buf);
192
193 for byte in buf.iter_mut() {
195 *byte = B64_CHARS[(*byte & 0x3f) as usize];
196 }
197}
198
199#[inline]
200fn atoi_u32(ascii: &[u8]) -> Option<u32> {
201 let mut out: u32 = 0;
202 for d in ascii.iter().map(|b| b.wrapping_sub(b'0')) {
203 if d < 10 {
204 out = out.saturating_mul(10).saturating_add(d as u32);
210 } else {
211 return None;
212 }
213 }
214 Some(out)
215}
216
217#[inline]
220fn push_u32_as_ascii(mut value: u32, output: &mut Vec<u8>) {
221 if value == 0 {
222 output.push(b'0');
223 return;
224 }
225
226 let mut digits = [0u8; 10]; let mut idx = digits.len() - 1;
229
230 while value > 0 {
231 (value, digits[idx]) = (value / 10, (value % 10) as u8 | b'0');
232 idx -= 1;
233 }
234
235 output.extend_from_slice(&digits[idx + 1..]);
236}
237
238macro_rules! b64_from_24bit {
243 ($b2:expr, $b1:expr, $b0:expr, $n:expr, $output:expr) => {{
244 let mut w = (($b2 as u32) << 16) | (($b1 as u32) << 8) | ($b0 as u32);
245 for _ in 0..$n {
246 $output.push(B64_CHARS[(w & 0x3f) as usize]);
247 w >>= 6;
248 }
249 }};
250}
251
252#[must_use = "the returned hash string is the result of expensive computation"]
297pub fn hash_with_salt(password: Password, salt: &[u8]) -> String {
298 let mut key_bytes = password.into_bytes();
299 let out = crypt_inner(&mut key_bytes, salt);
300 crypto::secure_zero_bytes(&mut key_bytes);
301 out
302}
303
304fn crypt_inner(key_bytes: &mut [u8], salt: &[u8]) -> String {
305 let mut salt = salt;
306 let mut rounds = ROUNDS_DEFAULT;
307 let mut rounds_custom = false;
308
309 if salt.starts_with(SHA512_SALT_PREFIX) {
311 salt = &salt[SHA512_SALT_PREFIX.len()..];
312 }
313
314 if salt.starts_with(SHA512_ROUNDS_PREFIX) {
316 let rest = &salt[SHA512_ROUNDS_PREFIX.len()..];
317 if let Some(dollar_pos) = rest.iter().position(|&b| b == b'$') {
318 if let Some(srounds) = atoi_u32(&rest[..dollar_pos]) {
319 salt = &rest[dollar_pos + 1..];
320 rounds = srounds.clamp(ROUNDS_MIN, ROUNDS_MAX);
321 rounds_custom = true;
322 }
323 }
324 }
325
326 let salt_len = salt
328 .iter()
329 .position(|&b| b == b'$')
330 .unwrap_or(salt.len())
331 .min(SALT_LEN_MAX);
332 let salt = &salt[..salt_len];
333
334 let mut ctx = Sha512Context::new();
336 let mut result;
337
338 ctx.update(key_bytes);
340 ctx.update(salt);
341 ctx.update(key_bytes);
342 result = ctx.finish();
343
344 ctx = Sha512Context::new();
346
347 ctx.update(key_bytes);
349
350 ctx.update(salt);
352
353 let mut cnt = key_bytes.len();
355 while cnt > 64 {
356 ctx.update(&result[..64]);
357 cnt -= 64;
358 }
359 ctx.update(&result[..cnt]);
360
361 cnt = key_bytes.len();
364 while cnt > 0 {
365 if (cnt & 1) != 0 {
366 ctx.update(&result[..64]);
367 } else {
368 ctx.update(key_bytes);
369 }
370 cnt >>= 1;
371 }
372
373 result = ctx.finish();
375
376 ctx = Sha512Context::new();
378 for _ in 0..key_bytes.len() {
379 ctx.update(key_bytes);
380 }
381 let temp_result = ctx.finish();
382
383 let mut p_bytes = Vec::with_capacity(key_bytes.len());
385 cnt = key_bytes.len();
386 while cnt >= 64 {
387 p_bytes.extend_from_slice(&temp_result[..64]);
388 cnt -= 64;
389 }
390 p_bytes.extend_from_slice(&temp_result[..cnt]);
391
392 ctx = Sha512Context::new();
394 for _ in 0..(16 + result[0] as usize) {
395 ctx.update(salt);
396 }
397 let temp_result = ctx.finish();
398
399 let mut s_bytes = Vec::with_capacity(salt.len());
401 cnt = salt.len();
402 while cnt >= 64 {
403 s_bytes.extend_from_slice(&temp_result[..64]);
404 cnt -= 64;
405 }
406 s_bytes.extend_from_slice(&temp_result[..cnt]);
407
408 for cnt in 0..rounds {
410 ctx = Sha512Context::new();
411
412 if (cnt & 1) != 0 {
414 ctx.update(&p_bytes);
415 } else {
416 ctx.update(&result[..64]);
417 }
418
419 if cnt % 3 != 0 {
421 ctx.update(&s_bytes);
422 }
423
424 if cnt % 7 != 0 {
426 ctx.update(&p_bytes);
427 }
428
429 if (cnt & 1) != 0 {
431 ctx.update(&result[..64]);
432 } else {
433 ctx.update(&p_bytes);
434 }
435
436 result = ctx.finish();
438 }
439
440 let output_size = salt_spec_output_size(salt.len(), rounds_custom) + SHA512_HASH_ENCODED_LENGTH;
442 let mut output: Vec<u8> = Vec::with_capacity(output_size);
443 output.extend_from_slice(SHA512_SALT_PREFIX);
444
445 if rounds_custom {
446 output.extend_from_slice(SHA512_ROUNDS_PREFIX);
447 push_u32_as_ascii(rounds, &mut output);
448 output.push(b'$');
449 }
450
451 output.extend_from_slice(salt);
452 output.push(b'$');
453
454 b64_from_24bit!(result[0], result[21], result[42], 4, &mut output);
456 b64_from_24bit!(result[22], result[43], result[1], 4, &mut output);
457 b64_from_24bit!(result[44], result[2], result[23], 4, &mut output);
458 b64_from_24bit!(result[3], result[24], result[45], 4, &mut output);
459 b64_from_24bit!(result[25], result[46], result[4], 4, &mut output);
460 b64_from_24bit!(result[47], result[5], result[26], 4, &mut output);
461 b64_from_24bit!(result[6], result[27], result[48], 4, &mut output);
462 b64_from_24bit!(result[28], result[49], result[7], 4, &mut output);
463 b64_from_24bit!(result[50], result[8], result[29], 4, &mut output);
464 b64_from_24bit!(result[9], result[30], result[51], 4, &mut output);
465 b64_from_24bit!(result[31], result[52], result[10], 4, &mut output);
466 b64_from_24bit!(result[53], result[11], result[32], 4, &mut output);
467 b64_from_24bit!(result[12], result[33], result[54], 4, &mut output);
468 b64_from_24bit!(result[34], result[55], result[13], 4, &mut output);
469 b64_from_24bit!(result[56], result[14], result[35], 4, &mut output);
470 b64_from_24bit!(result[15], result[36], result[57], 4, &mut output);
471 b64_from_24bit!(result[37], result[58], result[16], 4, &mut output);
472 b64_from_24bit!(result[59], result[17], result[38], 4, &mut output);
473 b64_from_24bit!(result[18], result[39], result[60], 4, &mut output);
474 b64_from_24bit!(result[40], result[61], result[19], 4, &mut output);
475 b64_from_24bit!(result[62], result[20], result[41], 4, &mut output);
476 b64_from_24bit!(0, 0, result[63], 2, &mut output);
477
478 crypto::secure_zero_bytes(&mut result);
482 crypto::secure_zero_bytes(&mut p_bytes);
483 crypto::secure_zero_bytes(&mut s_bytes);
484
485 unsafe { String::from_utf8_unchecked(output) }
489}
490
491#[must_use = "the returned hash string is the result of expensive computation"]
540pub fn hash(password: Password, rounds: Option<u32>) -> String {
541 let (r, r_custom) = match rounds {
542 None | Some(ROUNDS_DEFAULT) => (ROUNDS_DEFAULT, false),
543 Some(r) => (r, true),
544 };
545
546 let mut salt_spec: Vec<u8> = Vec::with_capacity(salt_spec_output_size(SALT_LEN_MAX, r_custom));
547 salt_spec.extend_from_slice(SHA512_SALT_PREFIX);
548
549 if r_custom {
550 salt_spec.extend_from_slice(SHA512_ROUNDS_PREFIX);
551 push_u32_as_ascii(r, &mut salt_spec);
552 salt_spec.push(b'$');
553 }
554
555 let salt_start = salt_spec.len();
559 salt_spec.resize(salt_start + SALT_LEN_MAX, 0);
560 generate_salt(&mut salt_spec[salt_start..]);
561
562 hash_with_salt(password, &salt_spec)
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
579pub struct InvalidHash;
580
581impl fmt::Display for InvalidHash {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 f.write_str("not a well-formed SHA512-crypt ($6$) hash")
584 }
585}
586
587impl core::error::Error for InvalidHash {}
588
589pub fn verify(password: Password, hash: &str) -> Result<bool, InvalidHash> {
643 let rest = hash
646 .strip_prefix(SHA512_SALT_PREFIX_STR)
647 .ok_or(InvalidHash)?;
648 let hash_start = rest.rfind('$').ok_or(InvalidHash)?;
649 let salt = &hash[..SHA512_SALT_PREFIX.len() + hash_start];
650
651 let computed = hash_with_salt(password, salt.as_bytes());
654
655 Ok(crypto::constant_time_eq(
656 computed.as_bytes(),
657 hash.as_bytes(),
658 ))
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use alloc::format;
665
666 #[test]
669 fn test_hello_world_basic() {
670 let result = hash_with_salt(Password::from("Hello world!"), b"$6$saltstring");
671 assert_eq!(
672 result,
673 "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"
674 );
675 }
676
677 #[test]
678 fn test_hello_world_with_rounds() {
679 let result = hash_with_salt(
680 Password::from("Hello world!"),
681 b"$6$rounds=10000$saltstringsaltstring",
682 );
683 assert_eq!(
684 result,
685 "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."
686 );
687 }
688
689 #[test]
690 fn test_long_salt_string() {
691 let result = hash_with_salt(
692 Password::from("This is just a test"),
693 b"$6$rounds=5000$toolongsaltstring",
694 );
695 assert_eq!(
696 result,
697 "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0"
698 );
699 }
700
701 #[test]
702 fn test_multiline_text() {
703 let result = hash_with_salt(
704 Password::from("a very much longer text to encrypt. This one even stretches over morethan one line."),
705 b"$6$rounds=1400$anotherlongsaltstring"
706 );
707 assert_eq!(
708 result,
709 "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1"
710 );
711 }
712
713 #[test]
714 fn test_short_salt() {
715 let result = hash_with_salt(
716 Password::from("we have a short salt string but not a short password"),
717 b"$6$rounds=77777$short",
718 );
719 assert_eq!(
720 result,
721 "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0"
722 );
723 }
724
725 #[test]
726 fn test_short_string() {
727 let result = hash_with_salt(
728 Password::from("a short string"),
729 b"$6$rounds=123456$asaltof16chars..",
730 );
731 assert_eq!(
732 result,
733 "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"
734 );
735 }
736
737 #[test]
738 fn test_rounds_minimum() {
739 let result = hash_with_salt(
740 Password::from("the minimum number is still observed"),
741 b"$6$rounds=10$roundstoolow",
742 );
743 assert_eq!(
744 result,
745 "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."
746 );
747 }
748
749 #[test]
752 fn test_verify_correct_password() {
753 let h = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
754 assert_eq!(verify(Password::from("Hello world!"), h), Ok(true));
755 }
756
757 #[test]
758 fn test_verify_wrong_password() {
759 let h = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
760 assert_eq!(verify(Password::from("wrong password"), h), Ok(false));
761 }
762
763 #[test]
764 fn test_verify_with_rounds() {
765 let h = "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.";
766 assert_eq!(verify(Password::from("Hello world!"), h), Ok(true));
767 assert_eq!(verify(Password::from("Hello world"), h), Ok(false)); }
769
770 #[test]
771 fn test_verify_invalid_hash_format() {
772 assert_eq!(
773 verify(Password::from("password"), "invalid_hash"),
774 Err(InvalidHash)
775 );
776 assert_eq!(
777 verify(Password::from("password"), "$5$saltstring$hash"),
778 Err(InvalidHash)
779 ); assert_eq!(
781 verify(Password::from("password"), "$6$nosalt"),
782 Err(InvalidHash)
783 ); }
785
786 #[test]
787 fn test_invalid_hash_display() {
788 let s = format!("{}", InvalidHash);
789 assert!(s.contains("$6$"));
790 }
791
792 #[test]
793 fn test_constant_time_comparison_smoke() {
794 let h1 = hash_with_salt(Password::from("password1"), b"$6$salt");
795 let h2 = hash_with_salt(Password::from("password2"), b"$6$salt");
796 assert_eq!(verify(Password::from("password1"), &h2), Ok(false));
797 assert_eq!(verify(Password::from("password2"), &h1), Ok(false));
798 }
799
800 #[test]
803 fn test_hash_default_rounds() {
804 let h = hash(Password::from("test_password"), None);
805 assert!(h.starts_with("$6$"));
806 assert!(!h.contains("rounds="));
807 assert_eq!(verify(Password::from("test_password"), &h), Ok(true));
808 assert_eq!(verify(Password::from("wrong_password"), &h), Ok(false));
809 }
810
811 #[test]
812 fn test_hash_custom_rounds() {
813 let h = hash(Password::from("test_password"), Some(10000));
814 assert!(h.starts_with("$6$rounds=10000$"));
815 assert_eq!(verify(Password::from("test_password"), &h), Ok(true));
816 assert_eq!(verify(Password::from("wrong_password"), &h), Ok(false));
817 }
818
819 #[test]
820 fn test_hash_different_salts() {
821 let h1 = hash(Password::from("test_password"), None);
822 let h2 = hash(Password::from("test_password"), None);
823 assert_ne!(h1, h2);
824 assert_eq!(verify(Password::from("test_password"), &h1), Ok(true));
825 assert_eq!(verify(Password::from("test_password"), &h2), Ok(true));
826 }
827
828 #[test]
829 fn test_hash_salt_length_and_alphabet() {
830 let h = hash(Password::from("test"), None);
831 let parts: Vec<&str> = h.splitn(4, '$').collect();
832 assert_eq!(parts.len(), 4);
833 assert_eq!(parts[1], "6");
834 let salt = parts[2];
835 assert_eq!(salt.len(), SALT_LEN_MAX);
836 for c in salt.chars() {
837 assert!(B64_CHARS.contains(&(c as u8)));
838 }
839 }
840
841 #[test]
842 fn test_hash_rounds_clamping() {
843 let h_low = hash(Password::from("test"), Some(100));
844 assert!(h_low.contains(&format!("rounds={}$", ROUNDS_MIN)));
845 let h_min = hash(Password::from("test"), Some(ROUNDS_MIN));
846 assert!(h_min.contains(&format!("rounds={}$", ROUNDS_MIN)));
847 let h_normal = hash(Password::from("test"), Some(10000));
848 assert!(h_normal.contains("rounds=10000$"));
849 assert_eq!(verify(Password::from("test"), &h_low), Ok(true));
850 assert_eq!(verify(Password::from("test"), &h_min), Ok(true));
851 assert_eq!(verify(Password::from("test"), &h_normal), Ok(true));
852 }
853
854 #[test]
855 fn test_hash_some_default_omits_rounds_segment() {
856 let h = hash(Password::from("x"), Some(ROUNDS_DEFAULT));
858 assert!(!h.contains("rounds="));
859 }
860
861 #[test]
862 fn test_hash_empty_password() {
863 let h = hash(Password::from(""), None);
864 assert_eq!(verify(Password::from(""), &h), Ok(true));
865 assert_eq!(verify(Password::from("not_empty"), &h), Ok(false));
866 }
867
868 #[test]
869 fn test_hash_unicode() {
870 let pw = "пароль🔐test";
871 let h = hash(Password::from(pw), Some(5000));
872 assert_eq!(verify(Password::from(pw), &h), Ok(true));
873 assert_eq!(verify(Password::from("wrong"), &h), Ok(false));
874 }
875
876 #[test]
879 fn test_salt_spec_output_size() {
880 let salt = "saltstring";
881 assert_eq!(salt_spec_output_size(salt.len(), false), 14);
883 assert_eq!(salt_spec_output_size(salt.len(), true), 31);
885 assert_eq!(salt_spec_output_size(SALT_LEN_MAX, true), 37);
887 }
888
889 #[test]
890 fn test_secure_zero_bytes() {
891 let mut v = alloc::vec![0x42u8; 128];
892 crypto::secure_zero_bytes(&mut v);
893 assert!(v.iter().all(|&b| b == 0));
894 }
895
896 #[test]
897 fn test_password_drop_zeros_buffer() {
898 let p = Password::from("secret");
904 drop(p);
905
906 let bytes = alloc::vec![0xAAu8; 32];
908 let p = Password::from_bytes(bytes);
909 let recovered = p.into_bytes();
911 assert!(recovered.iter().all(|&b| b == 0xAA));
912 }
913
914 #[test]
915 fn test_password_debug_does_not_leak() {
916 let p = Password::from("super-secret-value");
917 let dbg = format!("{:?}", p);
918 assert!(!dbg.contains("super-secret-value"));
919 assert!(dbg.contains("Password"));
920 }
921}