sha_crypt/
lib.rs

1//! Pure Rust implementation of the [`SHA-crypt` password hash based on SHA-512][1],
2//! a legacy password hashing scheme supported by the [POSIX crypt C library][2].
3//!
4//! Password hashes using this algorithm start with `$6$` when encoded using the
5//! [PHC string format][3].
6//!
7//! # Usage
8//!
9//! ```
10//! # #[cfg(feature = "simple")]
11//! # {
12//! use sha_crypt::{Sha512Params, sha512_simple, sha512_check};
13//!
14//! // First setup the Sha512Params arguments with:
15//! // rounds = 10_000
16//! let params = Sha512Params::new(10_000).expect("RandomError!");
17//!
18//! // Hash the password for storage
19//! let hashed_password = sha512_simple("Not so secure password", &params)
20//!     .expect("Should not fail");
21//!
22//! // Verifying a stored password
23//! assert!(sha512_check("Not so secure password", &hashed_password).is_ok());
24//! # }
25//! ```
26//!
27//! [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
28//! [2]: https://en.wikipedia.org/wiki/Crypt_(C)
29//! [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
30
31#![no_std]
32#![cfg_attr(docsrs, feature(doc_cfg))]
33#![doc(
34    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
35    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
36)]
37#![deny(unsafe_code)]
38#![warn(missing_docs, rust_2018_idioms)]
39
40// TODO(tarcieri): heapless support
41#[macro_use]
42extern crate alloc;
43
44#[cfg(feature = "std")]
45extern crate std;
46
47mod b64;
48mod defs;
49mod errors;
50mod params;
51
52pub use crate::{
53    defs::{BLOCK_SIZE_SHA256, BLOCK_SIZE_SHA512},
54    errors::CryptError,
55    params::{Sha256Params, Sha512Params, ROUNDS_DEFAULT, ROUNDS_MAX, ROUNDS_MIN},
56};
57
58use alloc::{string::String, vec::Vec};
59use sha2::{Digest, Sha256, Sha512};
60
61#[cfg(feature = "simple")]
62use {
63    crate::{
64        defs::{SALT_MAX_LEN, TAB},
65        errors::CheckError,
66    },
67    alloc::string::ToString,
68    rand::{distributions::Distribution, thread_rng, Rng},
69};
70
71#[cfg(feature = "simple")]
72static SHA256_SALT_PREFIX: &str = "$5$";
73#[cfg(feature = "simple")]
74static SHA256_ROUNDS_PREFIX: &str = "rounds=";
75
76#[cfg(feature = "simple")]
77static SHA512_SALT_PREFIX: &str = "$6$";
78#[cfg(feature = "simple")]
79static SHA512_ROUNDS_PREFIX: &str = "rounds=";
80
81/// The SHA512 crypt function returned as byte vector
82///
83/// If the provided hash is longer than defs::SALT_MAX_LEN character, it will
84/// be stripped down to defs::SALT_MAX_LEN characters.
85///
86/// # Arguments
87/// - `password` - The password to process as a byte vector
88/// - `salt` - The salt value to use as a byte vector
89/// - `params` - The Sha512Params to use
90///   **WARNING: Make sure to compare this value in constant time!**
91///
92/// # Returns
93/// - `Ok(())` if calculation was successful
94/// - `Err(errors::CryptError)` otherwise
95pub fn sha512_crypt(
96    password: &[u8],
97    salt: &[u8],
98    params: &Sha512Params,
99) -> Result<[u8; BLOCK_SIZE_SHA512], CryptError> {
100    let pw_len = password.len();
101
102    let salt_len = salt.len();
103    let salt = match salt_len {
104        0..=15 => &salt[0..salt_len],
105        _ => &salt[0..16],
106    };
107    let salt_len = salt.len();
108
109    if params.rounds < ROUNDS_MIN || params.rounds > ROUNDS_MAX {
110        return Err(CryptError::RoundsError);
111    }
112
113    let digest_a = sha512crypt_intermediate(password, salt);
114
115    // 13.
116    let mut hasher_alt = Sha512::default();
117
118    // 14.
119    for _ in 0..pw_len {
120        hasher_alt.update(password);
121    }
122
123    // 15.
124    let dp = hasher_alt.finalize();
125
126    // 16.
127    // Create byte sequence P.
128    let p_vec = produce_byte_seq(pw_len, &dp);
129
130    // 17.
131    hasher_alt = Sha512::default();
132
133    // 18.
134    // For every character in the password add the entire password.
135    for _ in 0..(16 + digest_a[0] as usize) {
136        hasher_alt.update(salt);
137    }
138
139    // 19.
140    // Finish the digest.
141    let ds = hasher_alt.finalize();
142
143    // 20.
144    // Create byte sequence S.
145    let s_vec = produce_byte_seq(salt_len, &ds);
146
147    let mut digest_c = digest_a;
148    // Repeatedly run the collected hash value through SHA512 to burn
149    // CPU cycles
150    for i in 0..params.rounds {
151        // new hasher
152        let mut hasher = Sha512::default();
153
154        // Add key or last result
155        if (i & 1) != 0 {
156            hasher.update(&p_vec);
157        } else {
158            hasher.update(digest_c);
159        }
160
161        // Add salt for numbers not divisible by 3
162        if i % 3 != 0 {
163            hasher.update(&s_vec);
164        }
165
166        // Add key for numbers not divisible by 7
167        if i % 7 != 0 {
168            hasher.update(&p_vec);
169        }
170
171        // Add key or last result
172        if (i & 1) != 0 {
173            hasher.update(digest_c);
174        } else {
175            hasher.update(&p_vec);
176        }
177
178        digest_c.clone_from_slice(&hasher.finalize());
179    }
180
181    Ok(digest_c)
182}
183
184/// The SHA256 crypt function returned as byte vector
185///
186/// If the provided hash is longer than defs::SALT_MAX_LEN character, it will
187/// be stripped down to defs::SALT_MAX_LEN characters.
188///
189/// # Arguments
190/// - `password` - The password to process as a byte vector
191/// - `salt` - The salt value to use as a byte vector
192/// - `params` - The Sha256Params to use
193///   **WARNING: Make sure to compare this value in constant time!**
194///
195/// # Returns
196/// - `Ok(())` if calculation was successful
197/// - `Err(errors::CryptError)` otherwise
198pub fn sha256_crypt(
199    password: &[u8],
200    salt: &[u8],
201    params: &Sha256Params,
202) -> Result<[u8; BLOCK_SIZE_SHA256], CryptError> {
203    let pw_len = password.len();
204
205    let salt_len = salt.len();
206    let salt = match salt_len {
207        0..=15 => &salt[0..salt_len],
208        _ => &salt[0..16],
209    };
210    let salt_len = salt.len();
211
212    if params.rounds < ROUNDS_MIN || params.rounds > ROUNDS_MAX {
213        return Err(CryptError::RoundsError);
214    }
215
216    let digest_a = sha256crypt_intermediate(password, salt);
217
218    // 13.
219    let mut hasher_alt = Sha256::default();
220
221    // 14.
222    for _ in 0..pw_len {
223        hasher_alt.update(password);
224    }
225
226    // 15.
227    let dp = hasher_alt.finalize();
228
229    // 16.
230    // Create byte sequence P.
231    let p_vec = produce_byte_seq(pw_len, &dp);
232
233    // 17.
234    hasher_alt = Sha256::default();
235
236    // 18.
237    // For every character in the password add the entire password.
238    for _ in 0..(16 + digest_a[0] as usize) {
239        hasher_alt.update(salt);
240    }
241
242    // 19.
243    // Finish the digest.
244    let ds = hasher_alt.finalize();
245
246    // 20.
247    // Create byte sequence S.
248    let s_vec = produce_byte_seq(salt_len, &ds);
249
250    let mut digest_c = digest_a;
251    // Repeatedly run the collected hash value through SHA256 to burn
252    // CPU cycles
253    for i in 0..params.rounds {
254        // new hasher
255        let mut hasher = Sha256::default();
256
257        // Add key or last result
258        if (i & 1) != 0 {
259            hasher.update(&p_vec);
260        } else {
261            hasher.update(digest_c);
262        }
263
264        // Add salt for numbers not divisible by 3
265        if i % 3 != 0 {
266            hasher.update(&s_vec);
267        }
268
269        // Add key for numbers not divisible by 7
270        if i % 7 != 0 {
271            hasher.update(&p_vec);
272        }
273
274        // Add key or last result
275        if (i & 1) != 0 {
276            hasher.update(digest_c);
277        } else {
278            hasher.update(&p_vec);
279        }
280
281        digest_c.clone_from_slice(&hasher.finalize());
282    }
283
284    Ok(digest_c)
285}
286
287/// Same as sha512_crypt except base64 representation will be returned.
288///
289/// # Arguments
290/// - `password` - The password to process as a byte vector
291/// - `salt` - The salt value to use as a byte vector
292/// - `params` - The Sha512Params to use
293///   **WARNING: Make sure to compare this value in constant time!**
294///
295/// # Returns
296/// - `Ok(())` if calculation was successful
297/// - `Err(errors::CryptError)` otherwise
298pub fn sha512_crypt_b64(
299    password: &[u8],
300    salt: &[u8],
301    params: &Sha512Params,
302) -> Result<String, CryptError> {
303    let output = sha512_crypt(password, salt, params)?;
304    let r = String::from_utf8(b64::encode_sha512(&output).to_vec())?;
305    Ok(r)
306}
307
308/// Same as sha256_crypt except base64 representation will be returned.
309///
310/// # Arguments
311/// - `password` - The password to process as a byte vector
312/// - `salt` - The salt value to use as a byte vector
313/// - `params` - The Sha256Params to use
314///   **WARNING: Make sure to compare this value in constant time!**
315///
316/// # Returns
317/// - `Ok(())` if calculation was successful
318/// - `Err(errors::CryptError)` otherwise
319pub fn sha256_crypt_b64(
320    password: &[u8],
321    salt: &[u8],
322    params: &Sha256Params,
323) -> Result<String, CryptError> {
324    let output = sha256_crypt(password, salt, params)?;
325    let r = String::from_utf8(b64::encode_sha256(&output).to_vec())?;
326    Ok(r)
327}
328
329/// Simple interface for generating a SHA512 password hash.
330///
331/// The salt will be chosen randomly. The output format will conform to [1].
332///
333///  `$<ID>$<SALT>$<HASH>`
334///
335/// # Returns
336/// - `Ok(String)` containing the full SHA512 password hash format on success
337/// - `Err(CryptError)` if something went wrong.
338///
339/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
340#[cfg(feature = "simple")]
341#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
342pub fn sha512_simple(password: &str, params: &Sha512Params) -> Result<String, CryptError> {
343    let rng = thread_rng();
344
345    let salt: String = rng
346        .sample_iter(&ShaCryptDistribution)
347        .take(SALT_MAX_LEN)
348        .collect();
349
350    let out = sha512_crypt(password.as_bytes(), salt.as_bytes(), params)?;
351
352    let mut result = String::new();
353    result.push_str(SHA512_SALT_PREFIX);
354    if params.rounds != ROUNDS_DEFAULT {
355        result.push_str(&format!("{}{}", SHA512_ROUNDS_PREFIX, params.rounds));
356        result.push('$');
357    }
358    result.push_str(&salt);
359    result.push('$');
360    let s = String::from_utf8(b64::encode_sha512(&out).to_vec())?;
361    result.push_str(&s);
362    Ok(result)
363}
364
365/// Simple interface for generating a SHA256 password hash.
366///
367/// The salt will be chosen randomly. The output format will conform to [1].
368///
369///  `$<ID>$<SALT>$<HASH>`
370///
371/// # Returns
372/// - `Ok(String)` containing the full SHA256 password hash format on success
373/// - `Err(CryptError)` if something went wrong.
374///
375/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
376#[cfg(feature = "simple")]
377#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
378pub fn sha256_simple(password: &str, params: &Sha256Params) -> Result<String, CryptError> {
379    let rng = thread_rng();
380
381    let salt: String = rng
382        .sample_iter(&ShaCryptDistribution)
383        .take(SALT_MAX_LEN)
384        .collect();
385
386    let out = sha256_crypt(password.as_bytes(), salt.as_bytes(), params)?;
387
388    let mut result = String::new();
389    result.push_str(SHA256_SALT_PREFIX);
390    if params.rounds != ROUNDS_DEFAULT {
391        result.push_str(&format!("{}{}", SHA256_ROUNDS_PREFIX, params.rounds));
392        result.push('$');
393    }
394    result.push_str(&salt);
395    result.push('$');
396    let s = String::from_utf8(b64::encode_sha256(&out).to_vec())?;
397    result.push_str(&s);
398    Ok(result)
399}
400
401/// Checks that given password matches provided hash.
402///
403/// # Arguments
404/// - `password` - expected password
405/// - `hashed_value` - the hashed value which should be used for checking,
406/// should be of format mentioned in [1]: `$6$<SALT>$<PWD>`
407///
408/// # Return
409/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid
410/// format or password mismatch.
411///
412/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
413#[cfg(feature = "simple")]
414#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
415pub fn sha512_check(password: &str, hashed_value: &str) -> Result<(), CheckError> {
416    let mut iter = hashed_value.split('$');
417
418    // Check that there are no characters before the first "$"
419    if iter.next() != Some("") {
420        return Err(CheckError::InvalidFormat(
421            "Should start with '$".to_string(),
422        ));
423    }
424
425    if iter.next() != Some("6") {
426        return Err(CheckError::InvalidFormat(format!(
427            "does not contain SHA512 identifier: '{SHA512_SALT_PREFIX}'",
428        )));
429    }
430
431    let mut next = iter.next().ok_or_else(|| {
432        CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string())
433    })?;
434    let rounds = if next.starts_with(SHA512_ROUNDS_PREFIX) {
435        let rounds = next;
436        next = iter.next().ok_or_else(|| {
437            CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string())
438        })?;
439
440        rounds[SHA512_ROUNDS_PREFIX.len()..].parse().map_err(|_| {
441            CheckError::InvalidFormat(format!(
442                "{SHA512_ROUNDS_PREFIX} specifier need to be a number",
443            ))
444        })?
445    } else {
446        ROUNDS_DEFAULT
447    };
448
449    let salt = next;
450
451    let hash = iter
452        .next()
453        .ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?;
454
455    // Make sure there is no trailing data after the final "$"
456    if iter.next().is_some() {
457        return Err(CheckError::InvalidFormat(
458            "Trailing characters present".to_string(),
459        ));
460    }
461
462    let params = Sha512Params { rounds };
463
464    let output = match sha512_crypt(password.as_bytes(), salt.as_bytes(), &params) {
465        Ok(v) => v,
466        Err(e) => return Err(CheckError::Crypt(e)),
467    };
468
469    let hash = b64::decode_sha512(hash.as_bytes())?;
470
471    use subtle::ConstantTimeEq;
472    if output.ct_eq(&hash).into() {
473        Ok(())
474    } else {
475        Err(CheckError::HashMismatch)
476    }
477}
478
479/// Checks that given password matches provided hash.
480///
481/// # Arguments
482/// - `password` - expected password
483/// - `hashed_value` - the hashed value which should be used for checking,
484/// should be of format mentioned in [1]: `$6$<SALT>$<PWD>`
485///
486/// # Return
487/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid
488/// format or password mismatch.
489///
490/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
491#[cfg(feature = "simple")]
492#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
493pub fn sha256_check(password: &str, hashed_value: &str) -> Result<(), CheckError> {
494    let mut iter = hashed_value.split('$');
495
496    // Check that there are no characters before the first "$"
497    if iter.next() != Some("") {
498        return Err(CheckError::InvalidFormat(
499            "Should start with '$".to_string(),
500        ));
501    }
502
503    if iter.next() != Some("5") {
504        return Err(CheckError::InvalidFormat(format!(
505            "does not contain SHA256 identifier: '{SHA256_SALT_PREFIX}'",
506        )));
507    }
508
509    let mut next = iter.next().ok_or_else(|| {
510        CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string())
511    })?;
512    let rounds = if next.starts_with(SHA256_ROUNDS_PREFIX) {
513        let rounds = next;
514        next = iter.next().ok_or_else(|| {
515            CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string())
516        })?;
517
518        rounds[SHA256_ROUNDS_PREFIX.len()..].parse().map_err(|_| {
519            CheckError::InvalidFormat(format!(
520                "{SHA256_ROUNDS_PREFIX} specifier need to be a number",
521            ))
522        })?
523    } else {
524        ROUNDS_DEFAULT
525    };
526
527    let salt = next;
528
529    let hash = iter
530        .next()
531        .ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?;
532
533    // Make sure there is no trailing data after the final "$"
534    if iter.next().is_some() {
535        return Err(CheckError::InvalidFormat(
536            "Trailing characters present".to_string(),
537        ));
538    }
539
540    let params = Sha256Params { rounds };
541
542    let output = match sha256_crypt(password.as_bytes(), salt.as_bytes(), &params) {
543        Ok(v) => v,
544        Err(e) => return Err(CheckError::Crypt(e)),
545    };
546
547    let hash = b64::decode_sha256(hash.as_bytes())?;
548
549    use subtle::ConstantTimeEq;
550    if output.ct_eq(&hash).into() {
551        Ok(())
552    } else {
553        Err(CheckError::HashMismatch)
554    }
555}
556
557#[cfg(feature = "simple")]
558#[derive(Debug)]
559struct ShaCryptDistribution;
560
561#[cfg(feature = "simple")]
562impl Distribution<char> for ShaCryptDistribution {
563    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
564        const RANGE: u32 = 26 + 26 + 10 + 2; // 2 == "./"
565        loop {
566            let var = rng.next_u32() >> (32 - 6);
567            if var < RANGE {
568                return TAB[var as usize] as char;
569            }
570        }
571    }
572}
573
574fn produce_byte_seq(len: usize, fill_from: &[u8]) -> Vec<u8> {
575    let bs = fill_from.len();
576    let mut seq: Vec<u8> = vec![0; len];
577    let mut offset: usize = 0;
578    for _ in 0..(len / bs) {
579        seq[offset..offset + bs].clone_from_slice(fill_from);
580        offset += bs;
581    }
582    let from_slice = &fill_from[..(len % bs)];
583    seq[offset..offset + (len % bs)].clone_from_slice(from_slice);
584    seq
585}
586
587fn sha512crypt_intermediate(password: &[u8], salt: &[u8]) -> [u8; BLOCK_SIZE_SHA512] {
588    let pw_len = password.len();
589
590    let mut hasher = Sha512::default();
591    hasher.update(password);
592    hasher.update(salt);
593
594    // 4.
595    let mut hasher_alt = Sha512::default();
596    // 5.
597    hasher_alt.update(password);
598    // 6.
599    hasher_alt.update(salt);
600    // 7.
601    hasher_alt.update(password);
602    // 8.
603    let digest_b = hasher_alt.finalize();
604
605    // 9.
606    for _ in 0..(pw_len / BLOCK_SIZE_SHA512) {
607        hasher.update(digest_b);
608    }
609    // 10.
610    hasher.update(&digest_b[..(pw_len % BLOCK_SIZE_SHA512)]);
611
612    // 11
613    let mut n = pw_len;
614    for _ in 0..pw_len {
615        if n == 0 {
616            break;
617        }
618        if (n & 1) != 0 {
619            hasher.update(digest_b);
620        } else {
621            hasher.update(password);
622        }
623        n >>= 1;
624    }
625
626    // 12.
627    hasher.finalize().as_slice().try_into().unwrap()
628}
629
630fn sha256crypt_intermediate(password: &[u8], salt: &[u8]) -> [u8; BLOCK_SIZE_SHA256] {
631    let pw_len = password.len();
632
633    let mut hasher = Sha256::default();
634    hasher.update(password);
635    hasher.update(salt);
636
637    // 4.
638    let mut hasher_alt = Sha256::default();
639    // 5.
640    hasher_alt.update(password);
641    // 6.
642    hasher_alt.update(salt);
643    // 7.
644    hasher_alt.update(password);
645    // 8.
646    let digest_b = hasher_alt.finalize();
647
648    // 9.
649    for _ in 0..(pw_len / BLOCK_SIZE_SHA256) {
650        hasher.update(digest_b);
651    }
652    // 10.
653    hasher.update(&digest_b[..(pw_len % BLOCK_SIZE_SHA256)]);
654
655    // 11
656    let mut n = pw_len;
657    for _ in 0..pw_len {
658        if n == 0 {
659            break;
660        }
661        if (n & 1) != 0 {
662            hasher.update(digest_b);
663        } else {
664            hasher.update(password);
665        }
666        n >>= 1;
667    }
668
669    // 12.
670    hasher.finalize().as_slice().try_into().unwrap()
671}