Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

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

7 changes: 6 additions & 1 deletion mfkdf2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ data-encoding = { version = "2.9.0", default-features = false, features = [
"alloc",
] }
regex = { version = "1.11.3", default-features = false }
zeroize = { version = "1.8.2", optional = true, default-features = false, features = [
"zeroize_derive",
"alloc",
] }


[target.'cfg(target_arch = "wasm32")'.dependencies]
Expand Down Expand Up @@ -159,10 +163,11 @@ name = "reconstitution"
path = "benches/reconstitution.rs"

[features]
default = []
default = ["zeroize"]
# Enable UniFFI bindings (FFI exports, scaffolding, bin, etc.)
bindings = ["dep:uniffi"]
differential-test = []
zeroize = ["dep:zeroize"]

[package.metadata.docs.rs]
rustdoc-args = ["--html-in-header", "katex-header.html"]
2 changes: 1 addition & 1 deletion mfkdf2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ let derive_password_factor = derive_password("password123")?;
# .finalize()
# .into_bytes()
# .into();
let derive_hmac_factor = derive_hmacsha1(response.into())?;
let derive_hmac_factor = derive_hmacsha1(response)?;
#
# let policy_hotp_factor = setup_derived_key
# .policy
Expand Down
20 changes: 10 additions & 10 deletions mfkdf2/benches/hmacsha1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use mfkdf2::{
derive,
setup::{
self,
factors::hmacsha1::{HmacSha1Options, HmacSha1Response, hmacsha1},
factors::hmacsha1::{HmacSha1Options, hmacsha1},
},
};

Expand Down Expand Up @@ -44,7 +44,7 @@ fn bench_hmacsha1(c: &mut Criterion) {
b.iter(|| {
let factors_map = black_box(HashMap::from([(
"hmac".to_string(),
derive::factors::hmacsha1(HmacSha1Response(SECRET20)).unwrap(),
derive::factors::hmacsha1(SECRET20).unwrap(),
)]));
let result = black_box(derive::key(&single_setup_key.policy, factors_map, false, false));
result.unwrap()
Expand Down Expand Up @@ -107,19 +107,19 @@ fn bench_hmacsha1(c: &mut Criterion) {
group.bench_function("multiple_derive_3", |b| {
b.iter(|| {
let factors_map = black_box(HashMap::from([
("hmac1".to_string(), derive::factors::hmacsha1(HmacSha1Response(SECRET20)).unwrap()),
("hmac1".to_string(), derive::factors::hmacsha1(SECRET20).unwrap()),
(
"hmac2".to_string(),
derive::factors::hmacsha1(HmacSha1Response([
derive::factors::hmacsha1([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
]))
])
.unwrap(),
),
(
"hmac3".to_string(),
derive::factors::hmacsha1(HmacSha1Response([
derive::factors::hmacsha1([
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
]))
])
.unwrap(),
),
]));
Expand Down Expand Up @@ -156,12 +156,12 @@ fn bench_hmacsha1(c: &mut Criterion) {
group.bench_function("threshold_derive_2_of_3", |b| {
b.iter(|| {
let factors_map = black_box(HashMap::from([
("hmac1".to_string(), derive::factors::hmacsha1(HmacSha1Response(SECRET20)).unwrap()),
("hmac1".to_string(), derive::factors::hmacsha1(SECRET20).unwrap()),
(
"hmac2".to_string(),
derive::factors::hmacsha1(HmacSha1Response([
derive::factors::hmacsha1([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
]))
])
.unwrap(),
),
]));
Expand Down
8 changes: 4 additions & 4 deletions mfkdf2/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub(crate) fn hkdf_sha256_with_info(input: &[u8], salt: &[u8], info: &[u8]) -> [
}

/// Encrypts a buffer using AES256-ECB with the given 32-byte key.
pub(crate) fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec<u8> {
pub(crate) fn encrypt(data: &[u8], key: impl AsRef<[u8]>) -> Vec<u8> {
// Ensure the input is a multiple of 16 by zero-padding if necessary.
let mut buf = {
let mut v = data.to_vec();
Expand All @@ -31,7 +31,7 @@ pub(crate) fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec<u8> {
v
};

let cipher = Encryptor::<Aes256>::new_from_slice(key).expect("Invalid AES-256 key");
let cipher = Encryptor::<Aes256>::new_from_slice(key.as_ref()).expect("Invalid AES-256 key");
let padded_len = buf.len(); // now guaranteed multiple of 16
cipher.encrypt_padded_mut::<NoPadding>(&mut buf, padded_len).expect("ECB encryption");
buf
Expand Down Expand Up @@ -63,8 +63,8 @@ where

/// Decrypts a buffer using AES256-ECB with the given 32-byte key.
// TODO (@lonerapier): check every use of decrypt and unpad properly or use assert.
pub(crate) fn decrypt(mut data: Vec<u8>, key: &[u8; 32]) -> Vec<u8> {
let cipher = Decryptor::<Aes256>::new_from_slice(key).expect("Invalid AES key");
pub(crate) fn decrypt(mut data: Vec<u8>, key: impl AsRef<[u8]>) -> Vec<u8> {
let cipher = Decryptor::<Aes256>::new_from_slice(key.as_ref()).expect("Invalid AES key");
let _ = cipher.decrypt_padded_mut::<NoPadding>(&mut data).expect("ECB decrypt");
data
}
Expand Down
64 changes: 36 additions & 28 deletions mfkdf2/src/definitions/bytearray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use serde::{Deserialize, Serialize};

/// Generic fixed-size byte array used as the basis for key-like types.
#[derive(Debug, Clone, PartialEq)]
pub struct ByteArray<const N: usize>(pub [u8; N]);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
pub struct ByteArray<const N: usize>([u8; N]);

/// 32 byte key
pub type Key = ByteArray<32>;
Expand Down Expand Up @@ -42,38 +43,45 @@ impl<const N: usize> std::ops::Deref for ByteArray<N> {
fn deref(&self) -> &Self::Target { &self.0 }
}

// Implement traits specifically for 32‑byte keys to satisfy serde and default bounds.
// Macro to implement Default, Serialize, and Deserialize for fixed-size ByteArrays
macro_rules! impl_bytearray {
($N:expr) => {
impl Default for ByteArray<$N> {
fn default() -> Self { ByteArray([0u8; $N]) }
}

impl Default for ByteArray<32> {
fn default() -> Self { ByteArray([0u8; 32]) }
}
impl Serialize for ByteArray<$N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_newtype_struct(stringify!(ByteArray), &self.0)
}
}

impl Serialize for ByteArray<32> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_newtype_struct("Key", &self.0)
}
}
impl<'de> Deserialize<'de> for ByteArray<$N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
struct ByteArrayVisitor;

impl<'de> Deserialize<'de> for ByteArray<32> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
struct KeyVisitor;
impl<'de> serde::de::Visitor<'de> for ByteArrayVisitor {
type Value = ByteArray<$N>;

impl<'de> serde::de::Visitor<'de> for KeyVisitor {
type Value = ByteArray<32>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a {}-byte array", $N)
}

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a 32-byte array for Key")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where D: serde::Deserializer<'de> {
let bytes = <[u8; $N]>::deserialize(deserializer)?;
Ok(ByteArray(bytes))
}
}

fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where D: serde::Deserializer<'de> {
let bytes = <[u8; 32]>::deserialize(deserializer)?;
Ok(ByteArray(bytes))
deserializer.deserialize_newtype_struct(stringify!(ByteArray), ByteArrayVisitor)
}
}

deserializer.deserialize_newtype_struct("Key", KeyVisitor)
}
};
}

// Implement for 20-byte and 32-byte arrays
impl_bytearray!(20);
impl_bytearray!(32);
2 changes: 1 addition & 1 deletion mfkdf2/src/definitions/entropy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
///
/// We recommend using "real" for most practical purposes. Entropy is only provided on key setup and
/// is not available on subsequent derivations.
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
pub struct MFKDF2Entropy {
/// Conservative estimate based on how the factor is actually produced or used. Calculated
/// using Dropbox's `zxcvbn` estimator.
Expand Down
4 changes: 3 additions & 1 deletion mfkdf2/src/definitions/factor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ pub(crate) trait FactorMetadata: Send + Sync + std::fmt::Debug {
/// assert_eq!(derive.data(), "password123".as_bytes());
/// # Ok::<(), mfkdf2::error::MFKDF2Error>(())
/// ```
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
#[derive(Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))]
pub struct MFKDF2Factor {
/// Optional application-defined identifier for the factor.
pub id: Option<String>,
Expand Down Expand Up @@ -99,6 +100,7 @@ impl std::fmt::Debug for MFKDF2Factor {
/// which define the common interface for factor management, setup, and derivation.
#[cfg_attr(feature = "bindings", derive(uniffi::Enum))]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))]
pub enum FactorType {
/// [`password::Password`] factor.
Password(password::Password),
Expand Down
12 changes: 10 additions & 2 deletions mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ impl crate::definitions::MFKDF2DerivedKey {
let purpose = purpose.unwrap_or("");

// derive internal key
let internal_key = self.derive_internal_key()?;
let mut internal_key = self.derive_internal_key()?;
// derive subkey
Ok(crate::crypto::hkdf_sha256_with_info(&internal_key, salt, purpose.as_bytes()))
let subkey = crate::crypto::hkdf_sha256_with_info(&internal_key, salt, purpose.as_bytes());

#[cfg(feature = "zeroize")]
{
use zeroize::Zeroize;
internal_key.zeroize();
}

Ok(subkey)
}
}

Expand Down
22 changes: 18 additions & 4 deletions mfkdf2/src/definitions/mfkdf_derived_key/hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl MFKDF2DerivedKey {
}

// derive internal key
let internal_key = self.derive_internal_key()?;
let mut internal_key = self.derive_internal_key()?;

let factor_data = self
.policy
Expand All @@ -52,13 +52,13 @@ impl MFKDF2DerivedKey {
.ok_or_else(|| MFKDF2Error::MissingFactor(factor_id.to_string()))?;
let pad = base64::Engine::decode(&general_purpose::STANDARD, factor_data.secret.as_bytes())?;
let salt = base64::Engine::decode(&general_purpose::STANDARD, factor_data.salt.as_bytes())?;
let secret_key = crate::crypto::hkdf_sha256_with_info(
let mut secret_key = crate::crypto::hkdf_sha256_with_info(
&internal_key,
&salt,
format!("mfkdf2:factor:secret:{factor_id}").as_bytes(),
);

let factor_material = crate::crypto::decrypt(pad, &secret_key);
let mut factor_material = crate::crypto::decrypt(pad, &secret_key);
let buffer = crate::crypto::hkdf_sha256_with_info(
&factor_material,
&salt,
Expand All @@ -70,6 +70,14 @@ impl MFKDF2DerivedKey {
acc
});

#[cfg(feature = "zeroize")]
{
use zeroize::Zeroize;
internal_key.zeroize();
secret_key.zeroize();
factor_material.zeroize();
}

Ok(
binary_string
.chars()
Expand Down Expand Up @@ -143,7 +151,7 @@ impl MFKDF2DerivedKey {
/// ```
pub fn add_hint(&mut self, factor_id: &str, bits: Option<u8>) -> Result<(), MFKDF2Error> {
// derive internal key
let internal_key = self.derive_internal_key()?;
let mut internal_key = self.derive_internal_key()?;

// verify policy integrity
if !self.policy.hmac.is_empty() {
Expand Down Expand Up @@ -175,6 +183,12 @@ impl MFKDF2DerivedKey {
self.policy.hmac = hmac;
}

#[cfg(feature = "zeroize")]
{
use zeroize::Zeroize;
internal_key.zeroize();
}

Ok(())
}
}
Expand Down
Loading