Skip to content

Commit 697b207

Browse files
authored
sha-crypt: extract simple module (#729)
Moves all functionality in the feature-gated `simple` API into its own module, which eliminates the need for a lot of gating, and means the "pure" functions sit in lib.rs without additional complications related to MCF encoding/decoding.
1 parent 5d0369c commit 697b207

File tree

3 files changed

+258
-265
lines changed

3 files changed

+258
-265
lines changed

sha-crypt/src/b64.rs

Lines changed: 0 additions & 31 deletions
This file was deleted.

sha-crypt/src/lib.rs

Lines changed: 4 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@
4040
#[macro_use]
4141
extern crate alloc;
4242

43-
mod b64;
4443
mod consts;
4544
mod errors;
4645
mod params;
46+
mod simple;
4747

4848
pub use crate::{
4949
consts::{BLOCK_SIZE_SHA256, BLOCK_SIZE_SHA512},
5050
errors::CryptError,
5151
params::{ROUNDS_DEFAULT, ROUNDS_MAX, ROUNDS_MIN, Sha256Params, Sha512Params},
5252
};
5353

54+
#[cfg(feature = "simple")]
55+
pub use crate::simple::{sha256_check, sha256_simple, sha512_check, sha512_simple};
56+
5457
use alloc::{string::String, vec::Vec};
5558
use base64ct::{Base64ShaCrypt, Encoding};
5659
use sha2::{Digest, Sha256, Sha512};
@@ -60,20 +63,6 @@ pub use crate::errors::{CheckError, DecodeError};
6063

6164
use crate::consts::{MAP_SHA256, MAP_SHA512};
6265

63-
#[cfg(feature = "simple")]
64-
use {
65-
crate::consts::SALT_MAX_LEN,
66-
alloc::string::ToString,
67-
rand_core::{OsRng, RngCore, TryRngCore},
68-
};
69-
70-
#[cfg(feature = "simple")]
71-
static SHA256_MCF_ID: &str = "5";
72-
#[cfg(feature = "simple")]
73-
static SHA512_MCF_ID: &str = "6";
74-
#[cfg(feature = "simple")]
75-
static ROUNDS_PARAM: &str = "rounds=";
76-
7766
/// The SHA512 crypt function returned as byte vector
7867
///
7968
/// If the provided hash is longer than defs::SALT_MAX_LEN character, it will
@@ -316,225 +305,6 @@ pub fn sha256_crypt_b64(password: &[u8], salt: &[u8], params: &Sha256Params) ->
316305
Base64ShaCrypt::encode_string(&transposed)
317306
}
318307

319-
/// Simple interface for generating a SHA512 password hash.
320-
///
321-
/// The salt will be chosen randomly. The output format will conform to [1].
322-
///
323-
/// `$<ID>$<SALT>$<HASH>`
324-
///
325-
/// # Returns
326-
/// - `Ok(String)` containing the full SHA512 password hash format on success
327-
/// - `Err(CryptError)` if something went wrong.
328-
///
329-
/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
330-
#[cfg(feature = "simple")]
331-
pub fn sha512_simple(password: &str, params: &Sha512Params) -> String {
332-
let salt = random_salt();
333-
let out = sha512_crypt_b64(password.as_bytes(), salt.as_bytes(), params);
334-
335-
let mut mcf_hash = mcf::PasswordHash::from_id(SHA512_MCF_ID).expect("should have valid ID");
336-
337-
if params.rounds != ROUNDS_DEFAULT {
338-
mcf_hash
339-
.push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds))
340-
.expect("should be valid field");
341-
}
342-
343-
mcf_hash.push_str(&salt).expect("should have valid salt");
344-
mcf_hash.push_str(&out).expect("should have valid hash");
345-
346-
mcf_hash.into()
347-
}
348-
349-
/// Simple interface for generating a SHA256 password hash.
350-
///
351-
/// The salt will be chosen randomly. The output format will conform to [1].
352-
///
353-
/// `$<ID>$<SALT>$<HASH>`
354-
///
355-
/// # Returns
356-
/// - `Ok(String)` containing the full SHA256 password hash format on success
357-
/// - `Err(CryptError)` if something went wrong.
358-
///
359-
/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
360-
#[cfg(feature = "simple")]
361-
pub fn sha256_simple(password: &str, params: &Sha256Params) -> String {
362-
let salt = random_salt();
363-
let out = sha256_crypt_b64(password.as_bytes(), salt.as_bytes(), params);
364-
365-
let mut mcf_hash = mcf::PasswordHash::from_id(SHA256_MCF_ID).expect("should have valid ID");
366-
367-
if params.rounds != ROUNDS_DEFAULT {
368-
mcf_hash
369-
.push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds))
370-
.expect("should be valid field");
371-
}
372-
373-
mcf_hash.push_str(&salt).expect("should have valid salt");
374-
mcf_hash.push_str(&out).expect("should have valid hash");
375-
376-
mcf_hash.into()
377-
}
378-
379-
/// Checks that given password matches provided hash.
380-
///
381-
/// # Arguments
382-
/// - `password` - expected password
383-
/// - `hashed_value` - the hashed value which should be used for checking,
384-
/// should be of format mentioned in [1]: `$6$<SALT>$<PWD>`
385-
///
386-
/// # Return
387-
/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid
388-
/// format or password mismatch.
389-
///
390-
/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
391-
#[cfg(feature = "simple")]
392-
pub fn sha512_check(password: &str, hashed_value: &str) -> Result<(), CheckError> {
393-
let mut iter = hashed_value.split('$');
394-
395-
// Check that there are no characters before the first "$"
396-
if iter.next() != Some("") {
397-
return Err(CheckError::InvalidFormat(
398-
"Should start with '$".to_string(),
399-
));
400-
}
401-
402-
if iter.next() != Some("6") {
403-
return Err(CheckError::InvalidFormat(format!(
404-
"does not contain SHA512 identifier: '${SHA512_MCF_ID}$'",
405-
)));
406-
}
407-
408-
let mut next = iter.next().ok_or_else(|| {
409-
CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string())
410-
})?;
411-
let rounds = if next.starts_with(ROUNDS_PARAM) {
412-
let rounds = next;
413-
next = iter.next().ok_or_else(|| {
414-
CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string())
415-
})?;
416-
417-
rounds[ROUNDS_PARAM.len()..].parse().map_err(|_| {
418-
CheckError::InvalidFormat(format!("{ROUNDS_PARAM} specifier need to be a number",))
419-
})?
420-
} else {
421-
ROUNDS_DEFAULT
422-
};
423-
424-
let salt = next;
425-
426-
let hash = iter
427-
.next()
428-
.ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?;
429-
430-
// Make sure there is no trailing data after the final "$"
431-
if iter.next().is_some() {
432-
return Err(CheckError::InvalidFormat(
433-
"Trailing characters present".to_string(),
434-
));
435-
}
436-
437-
let params = match Sha512Params::new(rounds) {
438-
Ok(p) => p,
439-
Err(e) => return Err(CheckError::Crypt(e)),
440-
};
441-
442-
let output = sha512_crypt(password.as_bytes(), salt.as_bytes(), &params);
443-
444-
let hash = b64::decode_sha512(hash.as_bytes())?;
445-
446-
use subtle::ConstantTimeEq;
447-
if output.ct_eq(&hash).into() {
448-
Ok(())
449-
} else {
450-
Err(CheckError::HashMismatch)
451-
}
452-
}
453-
454-
/// Checks that given password matches provided hash.
455-
///
456-
/// # Arguments
457-
/// - `password` - expected password
458-
/// - `hashed_value` - the hashed value which should be used for checking,
459-
/// should be of format mentioned in [1]: `$6$<SALT>$<PWD>`
460-
///
461-
/// # Return
462-
/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid
463-
/// format or password mismatch.
464-
///
465-
/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt
466-
#[cfg(feature = "simple")]
467-
pub fn sha256_check(password: &str, hashed_value: &str) -> Result<(), CheckError> {
468-
let mut iter = hashed_value.split('$');
469-
470-
// Check that there are no characters before the first "$"
471-
if iter.next() != Some("") {
472-
return Err(CheckError::InvalidFormat(
473-
"Should start with '$".to_string(),
474-
));
475-
}
476-
477-
if iter.next() != Some("5") {
478-
return Err(CheckError::InvalidFormat(format!(
479-
"does not contain SHA256 identifier: '${SHA256_MCF_ID}$'",
480-
)));
481-
}
482-
483-
let mut next = iter.next().ok_or_else(|| {
484-
CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string())
485-
})?;
486-
let rounds = if next.starts_with(ROUNDS_PARAM) {
487-
let rounds = next;
488-
next = iter.next().ok_or_else(|| {
489-
CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string())
490-
})?;
491-
492-
rounds[ROUNDS_PARAM.len()..].parse().map_err(|_| {
493-
CheckError::InvalidFormat(format!("{ROUNDS_PARAM} specifier need to be a number",))
494-
})?
495-
} else {
496-
ROUNDS_DEFAULT
497-
};
498-
499-
let salt = next;
500-
501-
let hash = iter
502-
.next()
503-
.ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?;
504-
505-
// Make sure there is no trailing data after the final "$"
506-
if iter.next().is_some() {
507-
return Err(CheckError::InvalidFormat(
508-
"Trailing characters present".to_string(),
509-
));
510-
}
511-
512-
let params = match Sha256Params::new(rounds) {
513-
Ok(p) => p,
514-
Err(e) => return Err(CheckError::Crypt(e)),
515-
};
516-
517-
let output = sha256_crypt(password.as_bytes(), salt.as_bytes(), &params);
518-
519-
let hash = b64::decode_sha256(hash.as_bytes())?;
520-
521-
use subtle::ConstantTimeEq;
522-
if output.ct_eq(&hash).into() {
523-
Ok(())
524-
} else {
525-
Err(CheckError::HashMismatch)
526-
}
527-
}
528-
529-
/// Generate a random salt that is 16-bytes long.
530-
#[cfg(feature = "simple")]
531-
fn random_salt() -> String {
532-
// Create buffer containing raw bytes to encode as Base64
533-
let mut buf = [0u8; (SALT_MAX_LEN * 3).div_ceil(4)];
534-
OsRng.unwrap_err().fill_bytes(&mut buf);
535-
Base64ShaCrypt::encode_string(&buf)
536-
}
537-
538308
fn produce_byte_seq(len: usize, fill_from: &[u8]) -> Vec<u8> {
539309
let bs = fill_from.len();
540310
let mut seq: Vec<u8> = vec![0; len];

0 commit comments

Comments
 (0)