Skip to main content

crypt_sha512/
lib.rs

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// --- Backend selection ---------------------------------------------------
9//
10// Exactly one `backend-*` feature must be enabled. The two checks below turn
11// "zero backends" and "two or more backends" into compile errors.
12//
13// We intentionally do not provide a default backend: each backend pulls in
14// substantially different system requirements (C toolchains, linked
15// libraries, target support), and silently picking one for the user has been
16// a recurring source of dependency-tree surprise in this corner of the
17// ecosystem.
18
19#[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// Tests and the rest of this file refer to crypto primitives via the
52// historical `crypto::` path; alias the chosen backend so we don't have to
53// touch every call site.
54#[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
77// Length of the base64-encoded SHA512 hash in the output
78const SHA512_HASH_ENCODED_LENGTH: usize = 86;
79
80// Custom base64 alphabet for crypt
81const B64_CHARS: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
82
83/// A password whose backing memory is securely zeroed when dropped.
84///
85/// This newtype is the primary way to pass passwords into [`hash`],
86/// [`hash_with_salt`], and [`verify`]. By accepting `Password` by value, the
87/// API guarantees that the caller's plaintext copy is destroyed (via
88/// `crypto::secure_zero_bytes()`) before the function returns, even on panic.
89///
90/// # Examples
91///
92/// ```
93/// use crypt_sha512::{hash, verify, Password};
94///
95/// let h = hash(Password::from("hunter2"), None);
96/// assert_eq!(verify(Password::from("hunter2"), &h), Ok(true));
97/// ```
98///
99/// `Password` deliberately does **not** implement [`Clone`], [`Debug`], or
100/// [`core::fmt::Display`] to discourage accidental duplication or logging of
101/// plaintext secrets.
102pub struct Password {
103    bytes: Vec<u8>,
104}
105
106impl Password {
107    /// Construct a `Password` from raw bytes. The provided `Vec<u8>` is moved
108    /// into the `Password` and will be zeroed when the `Password` is dropped.
109    #[inline]
110    pub fn from_bytes(bytes: Vec<u8>) -> Self {
111        Self { bytes }
112    }
113
114    /// Take ownership of the inner buffer without running the zeroing `Drop`.
115    /// Used internally so `crypt_inner` can mutate and then zero the bytes.
116    #[inline]
117    fn into_bytes(self) -> Vec<u8> {
118        // Move the bytes out, then forget self so Drop does not double-zero.
119        let mut me = core::mem::ManuallyDrop::new(self);
120        core::mem::take(&mut me.bytes)
121    }
122}
123
124impl From<String> for Password {
125    /// Move a `String`'s buffer into a `Password`. The original `String`'s
126    /// allocation becomes the `Password`'s allocation; no copy is made and the
127    /// buffer will be zeroed on drop.
128    #[inline]
129    fn from(s: String) -> Self {
130        Self {
131            bytes: s.into_bytes(),
132        }
133    }
134}
135
136impl From<&str> for Password {
137    /// Copy the string slice into a new `Password`. The copy will be zeroed on
138    /// drop, but the caller's original `&str` is unaffected — prefer
139    /// [`Password::from(String)`](#impl-From<String>-for-Password) when you
140    /// own the buffer.
141    #[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/// Calculate the exact buffer size needed for the salt-spec portion of the
169/// output hash string (everything before the encoded digest).
170#[inline(always)]
171fn salt_spec_output_size(salt_len: usize, rounds_custom: bool) -> usize {
172    // Base components:
173    // - SHA512_SALT_PREFIX ("$6$") = 3 bytes
174    // - salt + '$' separator = salt_len + 1
175    let mut size = SHA512_SALT_PREFIX.len() + salt_len + 1;
176
177    // Add rounds specification if custom:
178    // - SHA512_ROUNDS_PREFIX ("rounds=") = 7 bytes
179    // - maximum 9 digits for rounds value (up to 999999999)
180    // - '$' separator = 1 byte
181    if rounds_custom {
182        size += SHA512_ROUNDS_PREFIX.len() + 9 + 1;
183    }
184    size
185}
186
187/// Generate a random salt string for use with crypt_sha512
188/// Fills the provided buffer with random salt characters from B64_CHARS
189fn generate_salt(buf: &mut [u8]) {
190    // Generate random bytes directly into buffer
191    crypto::random_bytes(buf);
192
193    // Transform each byte using lower 6 bits to index into B64_CHARS
194    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            // Saturate on overflow: callers clamp the result into the
205            // SHA-crypt rounds range anyway, so saturating to u32::MAX is
206            // observationally identical to a correct (but unbounded)
207            // bignum parse for any input that fits in the spec's range,
208            // and well-defined for inputs that do not.
209            out = out.saturating_mul(10).saturating_add(d as u32);
210        } else {
211            return None;
212        }
213    }
214    Some(out)
215}
216
217/// Format a u32 as ASCII digits and append to output vector
218/// Uses div/mod arithmetic to avoid allocation
219#[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    // Extract digits using div/mod
227    let mut digits = [0u8; 10]; // Max 10 digits for u32
228    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
238/// Encode 3 bytes into 4 base64 characters (crypt-specific encoding).
239///
240/// Preserved as a macro to mirror the structure of Ulrich Drepper's reference
241/// C implementation, where the `b64_from_24bit` operation is also a macro.
242macro_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/// Compute a SHA512-crypt (`$6$`) hash with an explicit, caller-provided salt.
253///
254/// This is the low-level entry point, ported from Ulrich Drepper's reference
255/// SHA-crypt C implementation. For most callers, [`hash`] (which generates a
256/// secure random salt) is the better choice.
257///
258/// # Arguments
259///
260/// * `password` — Password to hash. The backing buffer is zeroed before this
261///   function returns (and on panic) thanks to [`Password`]'s `Drop`.
262/// * `salt` — Salt specification. Accepted forms:
263///   - `b"saltstring"` — bare salt, uses default 5000 rounds
264///   - `b"$6$saltstring"` — with the `$6$` prefix
265///   - `b"$6$rounds=10000$saltstring"` — with explicit rounds
266///
267///   Salt is truncated at the first `$` or at 16 bytes, whichever comes first.
268///   Rounds are silently clamped to the range `[1000, 999_999_999]` as
269///   specified by the SHA-crypt algorithm.
270///
271/// # Returns
272///
273/// A complete hash string of the form `$6$[rounds=N$]salt$hash`.
274///
275/// # Panics
276///
277/// Does not panic for any input (allocation failure aside).
278///
279/// # Security
280///
281/// - All intermediate sensitive buffers are wiped via the active backend's
282///   non-elidable zeroing primitive before this function returns.
283/// - The `password` buffer is zeroed before return (via `Password::Drop`).
284///
285/// # Examples
286///
287/// ```
288/// use crypt_sha512::{hash_with_salt, Password};
289///
290/// let h = hash_with_salt(Password::from("password"), b"saltstring");
291/// assert!(h.starts_with("$6$saltstring$"));
292///
293/// let h = hash_with_salt(Password::from("password"), b"$6$rounds=10000$saltstring");
294/// assert!(h.starts_with("$6$rounds=10000$saltstring$"));
295/// ```
296#[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    // Check for salt prefix
310    if salt.starts_with(SHA512_SALT_PREFIX) {
311        salt = &salt[SHA512_SALT_PREFIX.len()..];
312    }
313
314    // Check for rounds specification
315    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    // Extract salt (up to first $ or max length)
327    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    // Use a single SHA512 context throughout, reusing it for efficiency
335    let mut ctx = Sha512Context::new();
336    let mut result;
337
338    // Compute alternate SHA512 sum with input KEY, SALT, and KEY
339    ctx.update(key_bytes);
340    ctx.update(salt);
341    ctx.update(key_bytes);
342    result = ctx.finish();
343
344    // Prepare for the real work
345    ctx = Sha512Context::new();
346
347    // Add the key string
348    ctx.update(key_bytes);
349
350    // Add the salt
351    ctx.update(salt);
352
353    // Add for any character in the key one byte of the alternate sum
354    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    // Take the binary representation of the length of the key
362    // and for every 1 add the alternate sum, for every 0 the key
363    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    // Create intermediate result
374    result = ctx.finish();
375
376    // Start computation of P byte sequence
377    ctx = Sha512Context::new();
378    for _ in 0..key_bytes.len() {
379        ctx.update(key_bytes);
380    }
381    let temp_result = ctx.finish();
382
383    // Create byte sequence P
384    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    // Start computation of S byte sequence
393    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    // Create byte sequence S
400    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    // Repeatedly run the collected hash value through SHA512 to burn CPU cycles
409    for cnt in 0..rounds {
410        ctx = Sha512Context::new();
411
412        // Add key or last result
413        if (cnt & 1) != 0 {
414            ctx.update(&p_bytes);
415        } else {
416            ctx.update(&result[..64]);
417        }
418
419        // Add salt for numbers not divisible by 3
420        if cnt % 3 != 0 {
421            ctx.update(&s_bytes);
422        }
423
424        // Add key for numbers not divisible by 7
425        if cnt % 7 != 0 {
426            ctx.update(&p_bytes);
427        }
428
429        // Add key or last result
430        if (cnt & 1) != 0 {
431            ctx.update(&result[..64]);
432        } else {
433            ctx.update(&p_bytes);
434        }
435
436        // Create intermediate result
437        result = ctx.finish();
438    }
439
440    // Now we can construct the result string
441    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    // Encode the result in the specific order
455    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    // Clear sensitive memory to prevent information leakage. The backend's
479    // `secure_zero_bytes` is implemented so the compiler cannot elide the
480    // writes (see backend modules for details).
481    crypto::secure_zero_bytes(&mut result);
482    crypto::secure_zero_bytes(&mut p_bytes);
483    crypto::secure_zero_bytes(&mut s_bytes);
484
485    // SAFETY: every byte pushed to `output` is from the ASCII-only
486    // `B64_CHARS` table, the digit set produced by `push_u32_as_ascii`, or
487    // the literal ASCII bytes `$6$`, `rounds=`, and `$`.
488    unsafe { String::from_utf8_unchecked(output) }
489}
490
491/// Hash a password with a freshly generated, cryptographically secure random salt.
492///
493/// This is the recommended high-level API for storing new passwords.
494///
495/// # Arguments
496///
497/// * `password` — Password to hash. Its buffer is zeroed before return.
498/// * `rounds` — Optional iteration count:
499///   - `None` uses the SHA-crypt default of 5000 rounds and omits the
500///     `rounds=` segment from the output.
501///   - `Some(n)` records `rounds=n$` in the output, with `n` clamped into
502///     `[1000, 999_999_999]` per the SHA-crypt specification. Note that
503///     `Some(5000)` *also* omits the `rounds=` segment, so its output is
504///     bytewise identical to `None`.
505///
506/// # Returns
507///
508/// A complete hash string of the form `$6$[rounds=N$]salt$hash`.
509///
510/// # Panics
511///
512/// Panics (or aborts) if the active backend's CSPRNG fails. All four
513/// backends treat CSPRNG failure as fatal rather than returning a
514/// predictable salt:
515///
516/// - `backend-aws-lc` / `backend-boring`: the FFI `RAND_bytes` aborts the
517///   process when entropy is unavailable.
518/// - `backend-openssl`: this crate asserts the `RAND_bytes` return code.
519/// - `backend-rust-crypto`: this crate `expect`s the `getrandom` call.
520///
521/// # Security
522///
523/// - Salt is drawn from the active backend's CSPRNG.
524/// - Password and intermediate hash buffers are wiped via the backend's
525///   non-elidable zeroing primitive before this function returns.
526///
527/// # Examples
528///
529/// ```
530/// use crypt_sha512::{hash, verify, Password};
531///
532/// let h = hash(Password::from("hunter2"), None);
533/// assert_eq!(verify(Password::from("hunter2"), &h), Ok(true));
534///
535/// // Higher work factor for sensitive deployments
536/// let h = hash(Password::from("hunter2"), Some(100_000));
537/// assert_eq!(verify(Password::from("hunter2"), &h), Ok(true));
538/// ```
539#[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    // Append SALT_LEN_MAX random base64 chars. Zero-fill is negligible
556    // compared to the thousands of SHA-512 rounds that follow, and avoids
557    // any unsafe pointer juggling around `spare_capacity_mut`.
558    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/// Error returned by [`verify`] when the supplied hash string is not a
566/// well-formed SHA512-crypt (`$6$…$…`) value.
567///
568/// This is distinct from a simple password mismatch: a mismatch is reported
569/// as `Ok(false)`, while a malformed hash is reported as `Err(InvalidHash)`.
570///
571/// Distinguishing these cases is particularly useful when a credential
572/// store contains hashes from multiple algorithms (e.g. a legacy table
573/// holding a mix of `$1$` MD5-crypt, `$2y$` bcrypt, `$5$` SHA256-crypt,
574/// and `$6$` SHA512-crypt entries, or rows migrated from another system).
575/// `Err(InvalidHash)` lets the caller route the request to a different
576/// verifier — or surface data corruption — rather than treating a
577/// non-`$6$` hash as a failed authentication attempt.
578#[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
589/// Verify a password against a SHA512-crypt (`$6$`) hash using constant-time
590/// comparison.
591///
592/// Extracts the salt (and rounds, if present) from `hash`, recomputes the
593/// SHA512-crypt of `password` using that salt, and compares the result to
594/// `hash` with the active backend's constant-time comparison primitive.
595///
596/// # Arguments
597///
598/// * `password` — Password to check. Its buffer is zeroed before return,
599///   regardless of which branch is taken (match, mismatch, or parse error).
600/// * `hash` — Expected hash string in the format
601///   `$6$[rounds=N$]salt$encoded_digest`.
602///
603/// # Returns
604///
605/// * `Ok(true)` — `hash` is well-formed and `password` matches it.
606/// * `Ok(false)` — `hash` is well-formed and `password` does **not** match.
607/// * `Err(InvalidHash)` — `hash` is not a well-formed `$6$…$…` string. No
608///   password comparison was performed.
609///
610/// # Errors
611///
612/// Returns [`InvalidHash`] if `hash` does not begin with `$6$` or contains
613/// no `$` separating the salt from the encoded digest.
614///
615/// # Panics
616///
617/// Does not panic.
618///
619/// # Security
620///
621/// - Final comparison is constant-time (active backend's primitive).
622/// - Password buffer is zeroed via the backend's non-elidable zeroing
623///   primitive before return on every code path, including the
624///   malformed-hash early returns.
625///
626/// # Examples
627///
628/// ```
629/// use crypt_sha512::{hash, verify, InvalidHash, Password};
630///
631/// let h = hash(Password::from("correct horse battery staple"), None);
632/// assert_eq!(verify(Password::from("correct horse battery staple"), &h), Ok(true));
633/// assert_eq!(verify(Password::from("Tr0ub4dor&3"), &h), Ok(false));
634///
635/// // Malformed hash strings are surfaced as Err, not Ok(false).
636/// assert_eq!(verify(Password::from("anything"), "not a hash"), Err(InvalidHash));
637///
638/// // Works with any externally-produced $6$ hash:
639/// let h = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
640/// assert_eq!(verify(Password::from("Hello world!"), h), Ok(true));
641/// ```
642pub fn verify(password: Password, hash: &str) -> Result<bool, InvalidHash> {
643    // Extract the salt portion from the hash: $6$[rounds=N$]salt$digest.
644    // `password` is dropped (and zeroed) on every early return.
645    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    // Compute the hash with the extracted salt. `hash_with_salt` consumes
652    // and zeros the password.
653    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    // --- Algorithm vectors (from Drepper's reference test cases) ---
667
668    #[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    // --- verify() ---
750
751    #[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)); // missing !
768    }
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        ); // wrong algo
780        assert_eq!(
781            verify(Password::from("password"), "$6$nosalt"),
782            Err(InvalidHash)
783        ); // missing digest
784    }
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    // --- hash() ---
801
802    #[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        // Documented behavior: Some(ROUNDS_DEFAULT) is treated as None.
857        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    // --- helpers / invariants ---
877
878    #[test]
879    fn test_salt_spec_output_size() {
880        let salt = "saltstring";
881        // 3 ("$6$") + 10 + 1 = 14
882        assert_eq!(salt_spec_output_size(salt.len(), false), 14);
883        // 3 + 7 ("rounds=") + 9 + 1 + 10 + 1 = 31
884        assert_eq!(salt_spec_output_size(salt.len(), true), 31);
885        // 3 + 7 + 9 + 1 + 16 + 1 = 37
886        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        // Build a Password, take its bytes back out via the test-only path
899        // of From<Vec<u8>>::into to confirm Drop behavior indirectly: drop
900        // a Password over a buffer we can inspect afterward by reusing the
901        // allocation pattern. We can't directly observe freed memory, so
902        // instead exercise the public Drop path and confirm no panic.
903        let p = Password::from("secret");
904        drop(p);
905
906        // Direct check on the public byte-vec constructor:
907        let bytes = alloc::vec![0xAAu8; 32];
908        let p = Password::from_bytes(bytes);
909        // into_bytes does NOT zero (used internally) — sanity check it returns the data.
910        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}