Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions mcf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Pure Rust implementation of the Modular Crypt Format (MCF) which is used to stor
in the form `${id}$...`
"""

[dependencies]
base64ct = { version = "1.7", features = ["alloc"] }

[dev-dependencies]
hex-literal = "1"

[features]
default = ["alloc"]
alloc = []
95 changes: 95 additions & 0 deletions mcf/src/base64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Base64 encoding variants.

use base64ct::{Base64Bcrypt, Base64Crypt, Base64ShaCrypt, Encoding as _, Error as B64Error};

#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};

/// Base64 encoding variants used in various MCF encodings.
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Base64 {
/// bcrypt encoding.
///
/// ```text
/// ./ [A-Z] [a-z] [0-9]
/// 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39
/// ```
Bcrypt,

/// `crypt(3)` encoding.
///
/// ```text
/// [.-9] [A-Z] [a-z]
/// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
/// ```
Crypt,

/// `crypt(3)` Base64 encoding for the following schemes:
/// - sha1_crypt,
/// - sha256_crypt,
/// - sha512_crypt,
/// - md5_crypt
///
/// ```text
/// [.-9] [A-Z] [a-z]
/// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
/// ```
ShaCrypt,
}

impl Base64 {
/// Decode a Base64 string into the provided destination buffer.
pub fn decode(self, src: impl AsRef<[u8]>, dst: &mut [u8]) -> Result<&[u8], B64Error> {
match self {
Self::Bcrypt => Base64Bcrypt::decode(src, dst),
Self::Crypt => Base64Crypt::decode(src, dst),
Self::ShaCrypt => Base64ShaCrypt::decode(src, dst),
}
}

/// Decode a Base64 string into a byte vector.
#[cfg(feature = "alloc")]
pub fn decode_vec(self, input: &str) -> Result<Vec<u8>, B64Error> {
match self {
Self::Bcrypt => Base64Bcrypt::decode_vec(input),
Self::Crypt => Base64Crypt::decode_vec(input),
Self::ShaCrypt => Base64ShaCrypt::decode_vec(input),
}
}

/// Encode the input byte slice as Base64.
///
/// Writes the result into the provided destination slice, returning an
/// ASCII-encoded Base64 string value.
pub fn encode<'a>(self, src: &[u8], dst: &'a mut [u8]) -> Result<&'a str, B64Error> {
match self {
Self::Bcrypt => Base64Bcrypt::encode(src, dst),
Self::Crypt => Base64Crypt::encode(src, dst),
Self::ShaCrypt => Base64ShaCrypt::encode(src, dst),
}
.map_err(Into::into)
}

/// Encode input byte slice into a [`String`] containing Base64.
///
/// # Panics
/// If `input` length is greater than `usize::MAX/4`.
#[cfg(feature = "alloc")]
pub fn encode_string(self, input: &[u8]) -> String {
match self {
Self::Bcrypt => Base64Bcrypt::encode_string(input),
Self::Crypt => Base64Crypt::encode_string(input),
Self::ShaCrypt => Base64ShaCrypt::encode_string(input),
}
}

/// Get the length of Base64 produced by encoding the given bytes.
pub fn encoded_len(self, bytes: &[u8]) -> usize {
match self {
Self::Bcrypt => Base64Bcrypt::encoded_len(bytes),
Self::Crypt => Base64Crypt::encoded_len(bytes),
Self::ShaCrypt => Base64ShaCrypt::encoded_len(bytes),
}
}
}
25 changes: 25 additions & 0 deletions mcf/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Error types.

use core::fmt;

/// Result type for `mcf`.
pub type Result<T> = core::result::Result<T, Error>;

/// Error type.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Error {}

impl From<base64ct::Error> for Error {
fn from(_: base64ct::Error) -> Error {
Error {}
}
}

impl core::error::Error for Error {}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("modular crypt format error")
}
}
20 changes: 17 additions & 3 deletions mcf/src/fields.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Fields of an MCF password hash, delimited by `$`

use crate::{Error, Result};
use crate::{Base64, Error, Result};
use core::fmt;

#[cfg(feature = "alloc")]
use alloc::vec::Vec;

/// MCF field delimiter: `$`.
pub const DELIMITER: char = '$';

Expand Down Expand Up @@ -60,12 +63,23 @@ impl<'a> Field<'a> {
}

/// Borrow the field's contents as a `str`.
pub fn as_str(&self) -> &'a str {
pub fn as_str(self) -> &'a str {
self.0
}

/// Decode Base64 into the provided output buffer.
pub fn decode_base64_into(self, base64_variant: Base64, out: &mut [u8]) -> Result<&[u8]> {
Ok(base64_variant.decode(self.0, out)?)
}

/// Decode this field as the provided Base64 variant.
#[cfg(feature = "alloc")]
pub fn decode_base64(self, base64_variant: Base64) -> Result<Vec<u8>> {
Ok(base64_variant.decode_vec(self.0)?)
}

/// Validate a field in the password hash is well-formed.
pub(crate) fn validate(&self) -> Result<()> {
pub(crate) fn validate(self) -> Result<()> {
if self.0.is_empty() {
return Err(Error {});
}
Expand Down
27 changes: 8 additions & 19 deletions mcf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
#[cfg(feature = "alloc")]
extern crate alloc;

mod base64;
mod error;
mod fields;

pub use base64::Base64;
pub use error::{Error, Result};
pub use fields::{Field, Fields};

use core::fmt;

#[cfg(feature = "alloc")]
use {alloc::string::String, core::str};
use {
alloc::string::String,
core::{fmt, str},
};

/// Debug message used in panics when invariants aren't properly held.
#[cfg(feature = "alloc")]
Expand Down Expand Up @@ -155,19 +160,3 @@ fn validate_id(id: &str) -> Result<()> {

Ok(())
}

/// Result type for `mcf`.
pub type Result<T> = core::result::Result<T, Error>;

/// Error type.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Error {}

impl core::error::Error for Error {}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("modular crypt format error")
}
}
23 changes: 20 additions & 3 deletions mcf/tests/mcf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

#![cfg(feature = "alloc")]

use mcf::McfHash;
use hex_literal::hex;
use mcf::{Base64, McfHash};

#[test]
fn parse_malformed() {
Expand All @@ -25,10 +26,26 @@ fn parse_sha512_hash() {

let mut fields = hash.fields();
assert_eq!("rounds=100000", fields.next().unwrap().as_str());
assert_eq!("exn6tVc2j/MZD8uG", fields.next().unwrap().as_str());

let salt = fields.next().unwrap();
assert_eq!("exn6tVc2j/MZD8uG", salt.as_str());

let salt_bytes = salt.decode_base64(Base64::ShaCrypt).unwrap();
assert_eq!(&hex!("6a3f237988126f80958fa24b"), salt_bytes.as_slice());

let hash = fields.next().unwrap();
assert_eq!(
"BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0",
fields.next().unwrap().as_str()
hash.as_str()
);

let hash_bytes = hash.decode_base64(Base64::ShaCrypt).unwrap();
assert_eq!(
&hex!(
"0d358cad62739eb554863c183aef27e6390368fe061fc5fcb1193a392d60dcad4594fa8d383ab8fc3f0dc8088974602668422e6a58edfa1afe24831b10be69be"
),
hash_bytes.as_slice()
);

assert_eq!(None, fields.next());
}