Skip to content

Commit 61eaf9e

Browse files
authored
mcf: Base64 decoding support (#1918)
Adds a `Base64` enum with variants for `Bcrypt`, `Crypt`, and `ShaCrypt`, i.e. the non-standard Base64 variants used in MCF hashes. This is extracted from `password_hash::Encoding` which was straddling the line between the PHC format and MCF with such a feature. That type can now be removed in the next breaking release.
1 parent 2c3e02f commit 61eaf9e

File tree

7 files changed

+175
-25
lines changed

7 files changed

+175
-25
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mcf/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Pure Rust implementation of the Modular Crypt Format (MCF) which is used to stor
1414
in the form `${id}$...`
1515
"""
1616

17+
[dependencies]
18+
base64ct = { version = "1.7", features = ["alloc"] }
19+
20+
[dev-dependencies]
21+
hex-literal = "1"
22+
1723
[features]
1824
default = ["alloc"]
1925
alloc = []

mcf/src/base64.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Base64 encoding variants.
2+
3+
use base64ct::{Base64Bcrypt, Base64Crypt, Base64ShaCrypt, Encoding as _, Error as B64Error};
4+
5+
#[cfg(feature = "alloc")]
6+
use alloc::{string::String, vec::Vec};
7+
8+
/// Base64 encoding variants used in various MCF encodings.
9+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
10+
#[non_exhaustive]
11+
pub enum Base64 {
12+
/// bcrypt encoding.
13+
///
14+
/// ```text
15+
/// ./ [A-Z] [a-z] [0-9]
16+
/// 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39
17+
/// ```
18+
Bcrypt,
19+
20+
/// `crypt(3)` encoding.
21+
///
22+
/// ```text
23+
/// [.-9] [A-Z] [a-z]
24+
/// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
25+
/// ```
26+
Crypt,
27+
28+
/// `crypt(3)` Base64 encoding for the following schemes:
29+
/// - sha1_crypt,
30+
/// - sha256_crypt,
31+
/// - sha512_crypt,
32+
/// - md5_crypt
33+
///
34+
/// ```text
35+
/// [.-9] [A-Z] [a-z]
36+
/// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
37+
/// ```
38+
ShaCrypt,
39+
}
40+
41+
impl Base64 {
42+
/// Decode a Base64 string into the provided destination buffer.
43+
pub fn decode(self, src: impl AsRef<[u8]>, dst: &mut [u8]) -> Result<&[u8], B64Error> {
44+
match self {
45+
Self::Bcrypt => Base64Bcrypt::decode(src, dst),
46+
Self::Crypt => Base64Crypt::decode(src, dst),
47+
Self::ShaCrypt => Base64ShaCrypt::decode(src, dst),
48+
}
49+
}
50+
51+
/// Decode a Base64 string into a byte vector.
52+
#[cfg(feature = "alloc")]
53+
pub fn decode_vec(self, input: &str) -> Result<Vec<u8>, B64Error> {
54+
match self {
55+
Self::Bcrypt => Base64Bcrypt::decode_vec(input),
56+
Self::Crypt => Base64Crypt::decode_vec(input),
57+
Self::ShaCrypt => Base64ShaCrypt::decode_vec(input),
58+
}
59+
}
60+
61+
/// Encode the input byte slice as Base64.
62+
///
63+
/// Writes the result into the provided destination slice, returning an
64+
/// ASCII-encoded Base64 string value.
65+
pub fn encode<'a>(self, src: &[u8], dst: &'a mut [u8]) -> Result<&'a str, B64Error> {
66+
match self {
67+
Self::Bcrypt => Base64Bcrypt::encode(src, dst),
68+
Self::Crypt => Base64Crypt::encode(src, dst),
69+
Self::ShaCrypt => Base64ShaCrypt::encode(src, dst),
70+
}
71+
.map_err(Into::into)
72+
}
73+
74+
/// Encode input byte slice into a [`String`] containing Base64.
75+
///
76+
/// # Panics
77+
/// If `input` length is greater than `usize::MAX/4`.
78+
#[cfg(feature = "alloc")]
79+
pub fn encode_string(self, input: &[u8]) -> String {
80+
match self {
81+
Self::Bcrypt => Base64Bcrypt::encode_string(input),
82+
Self::Crypt => Base64Crypt::encode_string(input),
83+
Self::ShaCrypt => Base64ShaCrypt::encode_string(input),
84+
}
85+
}
86+
87+
/// Get the length of Base64 produced by encoding the given bytes.
88+
pub fn encoded_len(self, bytes: &[u8]) -> usize {
89+
match self {
90+
Self::Bcrypt => Base64Bcrypt::encoded_len(bytes),
91+
Self::Crypt => Base64Crypt::encoded_len(bytes),
92+
Self::ShaCrypt => Base64ShaCrypt::encoded_len(bytes),
93+
}
94+
}
95+
}

mcf/src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Error types.
2+
3+
use core::fmt;
4+
5+
/// Result type for `mcf`.
6+
pub type Result<T> = core::result::Result<T, Error>;
7+
8+
/// Error type.
9+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
10+
#[non_exhaustive]
11+
pub struct Error {}
12+
13+
impl From<base64ct::Error> for Error {
14+
fn from(_: base64ct::Error) -> Error {
15+
Error {}
16+
}
17+
}
18+
19+
impl core::error::Error for Error {}
20+
21+
impl fmt::Display for Error {
22+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23+
f.write_str("modular crypt format error")
24+
}
25+
}

mcf/src/fields.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Fields of an MCF password hash, delimited by `$`
22
3-
use crate::{Error, Result};
3+
use crate::{Base64, Error, Result};
44
use core::fmt;
55

6+
#[cfg(feature = "alloc")]
7+
use alloc::vec::Vec;
8+
69
/// MCF field delimiter: `$`.
710
pub const DELIMITER: char = '$';
811

@@ -60,12 +63,23 @@ impl<'a> Field<'a> {
6063
}
6164

6265
/// Borrow the field's contents as a `str`.
63-
pub fn as_str(&self) -> &'a str {
66+
pub fn as_str(self) -> &'a str {
6467
self.0
6568
}
6669

70+
/// Decode Base64 into the provided output buffer.
71+
pub fn decode_base64_into(self, base64_variant: Base64, out: &mut [u8]) -> Result<&[u8]> {
72+
Ok(base64_variant.decode(self.0, out)?)
73+
}
74+
75+
/// Decode this field as the provided Base64 variant.
76+
#[cfg(feature = "alloc")]
77+
pub fn decode_base64(self, base64_variant: Base64) -> Result<Vec<u8>> {
78+
Ok(base64_variant.decode_vec(self.0)?)
79+
}
80+
6781
/// Validate a field in the password hash is well-formed.
68-
pub(crate) fn validate(&self) -> Result<()> {
82+
pub(crate) fn validate(self) -> Result<()> {
6983
if self.0.is_empty() {
7084
return Err(Error {});
7185
}

mcf/src/lib.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616
#[cfg(feature = "alloc")]
1717
extern crate alloc;
1818

19+
mod base64;
20+
mod error;
1921
mod fields;
2022

23+
pub use base64::Base64;
24+
pub use error::{Error, Result};
2125
pub use fields::{Field, Fields};
2226

23-
use core::fmt;
24-
2527
#[cfg(feature = "alloc")]
26-
use {alloc::string::String, core::str};
28+
use {
29+
alloc::string::String,
30+
core::{fmt, str},
31+
};
2732

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

156161
Ok(())
157162
}
158-
159-
/// Result type for `mcf`.
160-
pub type Result<T> = core::result::Result<T, Error>;
161-
162-
/// Error type.
163-
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
164-
#[non_exhaustive]
165-
pub struct Error {}
166-
167-
impl core::error::Error for Error {}
168-
169-
impl fmt::Display for Error {
170-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171-
f.write_str("modular crypt format error")
172-
}
173-
}

mcf/tests/mcf.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
#![cfg(feature = "alloc")]
44

5-
use mcf::McfHash;
5+
use hex_literal::hex;
6+
use mcf::{Base64, McfHash};
67

78
#[test]
89
fn parse_malformed() {
@@ -25,10 +26,26 @@ fn parse_sha512_hash() {
2526

2627
let mut fields = hash.fields();
2728
assert_eq!("rounds=100000", fields.next().unwrap().as_str());
28-
assert_eq!("exn6tVc2j/MZD8uG", fields.next().unwrap().as_str());
29+
30+
let salt = fields.next().unwrap();
31+
assert_eq!("exn6tVc2j/MZD8uG", salt.as_str());
32+
33+
let salt_bytes = salt.decode_base64(Base64::ShaCrypt).unwrap();
34+
assert_eq!(&hex!("6a3f237988126f80958fa24b"), salt_bytes.as_slice());
35+
36+
let hash = fields.next().unwrap();
2937
assert_eq!(
3038
"BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0",
31-
fields.next().unwrap().as_str()
39+
hash.as_str()
3240
);
41+
42+
let hash_bytes = hash.decode_base64(Base64::ShaCrypt).unwrap();
43+
assert_eq!(
44+
&hex!(
45+
"0d358cad62739eb554863c183aef27e6390368fe061fc5fcb1193a392d60dcad4594fa8d383ab8fc3f0dc8088974602668422e6a58edfa1afe24831b10be69be"
46+
),
47+
hash_bytes.as_slice()
48+
);
49+
3350
assert_eq!(None, fields.next());
3451
}

0 commit comments

Comments
 (0)