diff --git a/base64ct/README.md b/base64ct/README.md index 252b03612..c6b00541f 100644 --- a/base64ct/README.md +++ b/base64ct/README.md @@ -40,6 +40,7 @@ fixed-width line wrapping. - URL-safe Base64: `[A-Z]`, `[a-z]`, `[0-9]`, `-`, `_` - bcrypt Base64: `.`, `/`, `[A-Z]`, `[a-z]`, `[0-9]` - `crypt(3)` Base64: `.`, `-`, `[0-9]`, `[A-Z]`, `[a-z]` +- PBKDF2 Base64: `[A-Z]`, `[a-z]`, `[0-9]`, `.`, `/` ## Minimum Supported Rust Version (MSRV) Policy diff --git a/base64ct/src/alphabet.rs b/base64ct/src/alphabet.rs index 78d92d7f6..a1753805f 100644 --- a/base64ct/src/alphabet.rs +++ b/base64ct/src/alphabet.rs @@ -7,6 +7,7 @@ use core::{fmt::Debug, ops::RangeInclusive}; pub mod bcrypt; pub mod crypt; +pub mod pbkdf2; pub mod shacrypt; pub mod standard; pub mod url; diff --git a/base64ct/src/alphabet/pbkdf2.rs b/base64ct/src/alphabet/pbkdf2.rs new file mode 100644 index 000000000..e772794a5 --- /dev/null +++ b/base64ct/src/alphabet/pbkdf2.rs @@ -0,0 +1,33 @@ +use crate::alphabet::{Alphabet, DecodeStep, EncodeStep}; + +/// PBKDF2 Base64: variant of unpadded standard Base64 with `.` instead of `+`. +/// +/// ```text +/// [A-Z] [a-z] [0-9] . / +/// 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2e, 0x2f +/// ``` +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Base64Pbkdf2; + +impl Alphabet for Base64Pbkdf2 { + const BASE: u8 = b'A'; + const DECODER: &'static [DecodeStep] = DECODER; + const ENCODER: &'static [EncodeStep] = ENCODER; + const PADDED: bool = false; + type Unpadded = Self; +} + +const DECODER: &[DecodeStep] = &[ + DecodeStep::Range(b'A'..=b'Z', -64), + DecodeStep::Range(b'a'..=b'z', -70), + DecodeStep::Range(b'0'..=b'9', 5), + DecodeStep::Eq(b'.', 63), + DecodeStep::Eq(b'/', 64), +]; + +const ENCODER: &[EncodeStep] = &[ + EncodeStep::Diff(25, 6), + EncodeStep::Diff(51, -75), + EncodeStep::Diff(61, -(b'.' as i16 - 0x22)), + EncodeStep::Diff(62, b'/' as i16 - b'.' as i16 - 1), +]; diff --git a/base64ct/src/lib.rs b/base64ct/src/lib.rs index 5532b8f91..2c44c6132 100644 --- a/base64ct/src/lib.rs +++ b/base64ct/src/lib.rs @@ -89,6 +89,7 @@ mod test_vectors; pub use crate::{ alphabet::{ bcrypt::Base64Bcrypt, + pbkdf2::Base64Pbkdf2, shacrypt::Base64ShaCrypt, standard::{Base64, Base64Unpadded}, url::{Base64Url, Base64UrlUnpadded}, diff --git a/base64ct/tests/pbkdf2.rs b/base64ct/tests/pbkdf2.rs new file mode 100644 index 000000000..543a836ab --- /dev/null +++ b/base64ct/tests/pbkdf2.rs @@ -0,0 +1,78 @@ +//! PBKDF2 MCF variant of Base64 + +#[macro_use] +mod common; + +use crate::common::*; +use base64ct::Base64Pbkdf2; + +const TEST_VECTORS: &[TestVector] = &[ + TestVector { raw: b"", b64: "" }, + TestVector { + raw: b"\0", + b64: "AA", + }, + TestVector { + raw: b"***", + b64: "Kioq", + }, + TestVector { + raw: b"\x01\x02\x03\x04", + b64: "AQIDBA", + }, + TestVector { + raw: b"\xAD\xAD\xAD\xAD\xAD", + b64: "ra2tra0", + }, + TestVector { + raw: b"\xFF\xEF\xFE\xFF\xEF\xFE", + b64: "/./././.", + }, + TestVector { + raw: b"\xFF\xFF\xFF\xFF\xFF", + b64: "//////8", + }, + TestVector { + raw: b"\x40\xC1\x3F\xBD\x05\x4C\x72\x2A\xA3\xC2\xF2\x11\x73\xC0\x69\xEA\ + \x49\x7D\x35\x29\x6B\xCC\x24\x65\xF6\xF9\xD0\x41\x08\x7B\xD7\xA9", + b64: "QME/vQVMciqjwvIRc8Bp6kl9NSlrzCRl9vnQQQh716k", + }, + TestVector { + raw: b"\x00\x10\x83\x10Q\x87 \x92\x8B0\xD3\x8FA\x14\x93QU\x97a\x96\x9Bq\ + \xD7\x9F\x82\x18\xA3\x92Y\xA7\xA2\x9A\xAB\xB2\xDB\xAF\xC3\x1C\xB3\ + \xFB\xF0\x00", + b64: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./AA", + }, +]; + +impl_tests!(Base64Pbkdf2); + +#[test] +fn reject_trailing_whitespace() { + let input = "EA2zjEJAQWeXkj6FQw/duYZxBGZfn0FZxjbEEEVvpuY\n"; + let mut buf = [0u8; 1024]; + assert_eq!( + Base64Pbkdf2::decode(input, &mut buf), + Err(Error::InvalidEncoding) + ); +} + +#[test] +fn reject_trailing_equals() { + let input = "EA2zjEJAQWeXkj6FQw/duYZxBGZfn0FZxjbEEEVvpuY="; + let mut buf = [0u8; 1024]; + assert_eq!( + Base64Pbkdf2::decode(input, &mut buf), + Err(Error::InvalidEncoding) + ); +} + +#[test] +fn reject_non_canonical_encoding() { + let input = "Mi"; + let mut buf = [0u8; 8]; + assert_eq!( + Base64Pbkdf2::decode(input, &mut buf), + Err(Error::InvalidEncoding) + ); +} diff --git a/mcf/src/base64.rs b/mcf/src/base64.rs index 6ca108213..a08e2835a 100644 --- a/mcf/src/base64.rs +++ b/mcf/src/base64.rs @@ -3,7 +3,8 @@ #![cfg(feature = "base64")] use base64ct::{ - Base64Bcrypt, Base64ShaCrypt, Base64Unpadded as B64, Encoding as _, Error as B64Error, + Base64Bcrypt, Base64Pbkdf2, Base64ShaCrypt, Base64Unpadded as B64, Encoding as _, + Error as B64Error, }; #[cfg(feature = "alloc")] @@ -45,6 +46,16 @@ pub enum Base64 { /// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a /// ``` Crypt, + + /// PBKDF2 Base64 encoding. + /// + /// This is a variant of the unpadded standard Base64 with `.` in place of `+`: + /// + /// ```text + /// [A-Z] [a-z] [0-9] . / + /// 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2e, 0x2f + /// ``` + Pbkdf2, } impl Base64 { @@ -54,6 +65,7 @@ impl Base64 { Self::B64 => B64::decode(src, dst), Self::Bcrypt => Base64Bcrypt::decode(src, dst), Self::Crypt => Base64ShaCrypt::decode(src, dst), + Self::Pbkdf2 => Base64Pbkdf2::decode(src, dst), } } @@ -64,6 +76,7 @@ impl Base64 { Self::B64 => B64::decode_vec(input), Self::Bcrypt => Base64Bcrypt::decode_vec(input), Self::Crypt => Base64ShaCrypt::decode_vec(input), + Self::Pbkdf2 => Base64Pbkdf2::decode_vec(input), } } @@ -76,6 +89,7 @@ impl Base64 { Self::B64 => B64::encode(src, dst), Self::Bcrypt => Base64Bcrypt::encode(src, dst), Self::Crypt => Base64ShaCrypt::encode(src, dst), + Self::Pbkdf2 => Base64Pbkdf2::encode(src, dst), } .map_err(Into::into) } @@ -90,6 +104,7 @@ impl Base64 { Self::B64 => B64::encode_string(input), Self::Bcrypt => Base64Bcrypt::encode_string(input), Self::Crypt => Base64ShaCrypt::encode_string(input), + Self::Pbkdf2 => Base64Pbkdf2::encode_string(input), } } @@ -99,6 +114,7 @@ impl Base64 { Self::B64 => B64::encoded_len(bytes), Self::Bcrypt => Base64Bcrypt::encoded_len(bytes), Self::Crypt => Base64ShaCrypt::encoded_len(bytes), + Self::Pbkdf2 => Base64Pbkdf2::encoded_len(bytes), } } }