From 9fc5dd6649fbf9d6fed8791d7183cb7725690e9a Mon Sep 17 00:00:00 2001 From: lonerapier Date: Wed, 3 Dec 2025 14:37:46 +0530 Subject: [PATCH 1/8] fix(hmacsha1): remove `params` from factor, add zeroize, accept impl type --- Cargo.lock | 1 + mfkdf2/Cargo.toml | 7 ++++++- mfkdf2/benches/hmacsha1.rs | 20 ++++++++++---------- mfkdf2/src/definitions/bytearray.rs | 1 + mfkdf2/src/definitions/entropy.rs | 3 ++- mfkdf2/src/derive/factors/hmacsha1.rs | 9 +++------ mfkdf2/src/derive/key.rs | 4 ++-- mfkdf2/src/setup/factors/hmacsha1.rs | 23 +++++++++-------------- mfkdf2/tests/common/mod.rs | 2 +- mfkdf2/tests/integration.rs | 5 ++--- mfkdf2/tests/integrity.rs | 12 ++++++------ mfkdf2/tests/stack.rs | 7 ++----- 12 files changed, 45 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5d61042..d57fd167 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,6 +1001,7 @@ dependencies = [ "uniffi", "uuid", "web-time", + "zeroize", "zxcvbn", ] diff --git a/mfkdf2/Cargo.toml b/mfkdf2/Cargo.toml index 61324f07..662fbb14 100644 --- a/mfkdf2/Cargo.toml +++ b/mfkdf2/Cargo.toml @@ -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] @@ -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"] diff --git a/mfkdf2/benches/hmacsha1.rs b/mfkdf2/benches/hmacsha1.rs index a6152095..abe7a852 100644 --- a/mfkdf2/benches/hmacsha1.rs +++ b/mfkdf2/benches/hmacsha1.rs @@ -6,7 +6,7 @@ use mfkdf2::{ derive, setup::{ self, - factors::hmacsha1::{HmacSha1Options, HmacSha1Response, hmacsha1}, + factors::hmacsha1::{HmacSha1Options, hmacsha1}, }, }; @@ -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() @@ -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(), ), ])); @@ -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(), ), ])); diff --git a/mfkdf2/src/definitions/bytearray.rs b/mfkdf2/src/definitions/bytearray.rs index f8a4d898..704e3d4b 100644 --- a/mfkdf2/src/definitions/bytearray.rs +++ b/mfkdf2/src/definitions/bytearray.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; /// Generic fixed-size byte array used as the basis for key-like types. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct ByteArray(pub [u8; N]); /// 32 byte key diff --git a/mfkdf2/src/definitions/entropy.rs b/mfkdf2/src/definitions/entropy.rs index 1f3c75c5..5bfd6a76 100644 --- a/mfkdf2/src/definitions/entropy.rs +++ b/mfkdf2/src/definitions/entropy.rs @@ -7,8 +7,9 @@ /// /// 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))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct MFKDF2Entropy { /// Conservative estimate based on how the factor is actually produced or used. Calculated /// using Dropbox's `zxcvbn` estimator. diff --git a/mfkdf2/src/derive/factors/hmacsha1.rs b/mfkdf2/src/derive/factors/hmacsha1.rs index 725f3040..98472891 100644 --- a/mfkdf2/src/derive/factors/hmacsha1.rs +++ b/mfkdf2/src/derive/factors/hmacsha1.rs @@ -21,8 +21,6 @@ impl FactorDerive for HmacSha1 { /// Includes the public parameters for in factor state and decrypts the secret material. fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> { - self.params = Some(serde_json::to_string(¶ms).unwrap()); - let response = self.response.as_ref().unwrap(); let mut padded_key = [0u8; 32]; padded_key[..response.0.len()].copy_from_slice(&response.0); @@ -130,12 +128,11 @@ impl FactorDerive for HmacSha1 { /// assert_eq!(derived_key.key, setup_key.key); /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn hmacsha1(response: HmacSha1Response) -> MFKDF2Result { +pub fn hmacsha1(response: impl Into) -> MFKDF2Result { Ok(MFKDF2Factor { id: None, factor_type: FactorType::HmacSha1(HmacSha1 { - response: Some(response), - params: None, + response: Some(response.into()), padded_secret: [0u8; 32].to_vec(), }), entropy: None, @@ -180,7 +177,7 @@ mod tests { .collect::>(); let response = crate::crypto::hmacsha1(&secret, &challenge); - let result = hmacsha1(response.into()); + let result = hmacsha1(response); assert!(result.is_ok()); result.unwrap().factor_type } diff --git a/mfkdf2/src/derive/key.rs b/mfkdf2/src/derive/key.rs index 9f32d85f..2dcfc819 100644 --- a/mfkdf2/src/derive/key.rs +++ b/mfkdf2/src/derive/key.rs @@ -590,7 +590,7 @@ mod tests { panic!() }; let response = crate::crypto::hmacsha1(secret, &challenge); - let mut derive_hmac_factor = derive_hmacsha1(response.into()).unwrap(); + let mut derive_hmac_factor = derive_hmacsha1(response).unwrap(); derive_hmac_factor.id = Some("hmac".to_string()); derive_factors_map.insert("hmac".to_string(), derive_hmac_factor); @@ -822,7 +822,7 @@ mod tests { let challenge = hex::decode(params["challenge"].as_str().unwrap()).unwrap(); let secret = &hmac_setup.padded_secret[..20]; let response = crate::crypto::hmacsha1(secret, &challenge); - let mut derive_hmac_factor = derive_hmacsha1(response.into()).unwrap(); + let mut derive_hmac_factor = derive_hmacsha1(response).unwrap(); derive_hmac_factor.id = Some("hmac".to_string()); derive_factors_map.insert("hmac".to_string(), derive_hmac_factor); diff --git a/mfkdf2/src/setup/factors/hmacsha1.rs b/mfkdf2/src/setup/factors/hmacsha1.rs index c36b4e23..c9088e6d 100644 --- a/mfkdf2/src/setup/factors/hmacsha1.rs +++ b/mfkdf2/src/setup/factors/hmacsha1.rs @@ -18,7 +18,7 @@ use serde_json::{Value, json}; use crate::{ crypto::encrypt, - definitions::{Key, MFKDF2Factor}, + definitions::{ByteArray, Key, MFKDF2Factor}, error::MFKDF2Result, rng, setup::factors::{FactorMetadata, FactorSetup, FactorType}, @@ -31,8 +31,9 @@ use crate::{ /// resulting key into a dedicated HMAC‑SHA1 slot on the token; /// - or supply a 20‑byte key that you have already provisioned into the device so that both MFKDF2 /// and the token agree on the same `kₜ`. -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct HmacSha1Options { /// Optional application-defined identifier for the factor. Defaults to `"hmacsha1"`. If /// provided, it must be non-empty. @@ -46,12 +47,7 @@ impl Default for HmacSha1Options { } /// HMAC‑SHA1 response -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct HmacSha1Response(pub [u8; 20]); - -impl From<[u8; 20]> for HmacSha1Response { - fn from(value: [u8; 20]) -> Self { HmacSha1Response(value) } -} +pub(crate) type HmacSha1Response = ByteArray<20>; /// HMAC‑SHA1 factor state. /// @@ -60,11 +56,10 @@ impl From<[u8; 20]> for HmacSha1Response { /// that the derive side can use to confirm it has the same secret. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct HmacSha1 { /// HMAC‑SHA1 response pub response: Option, - /// Public parameters for the factor - pub params: Option, /// Padded HMAC key. 20 bytes of secret + 12 bytes of padding. pub padded_secret: Vec, } @@ -144,16 +139,16 @@ impl FactorSetup for HmacSha1 { /// let setup_key = setup::key(&[factor], MFKDF2Options::default())?; /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn hmacsha1(options: HmacSha1Options) -> MFKDF2Result { +pub fn hmacsha1(mut options: HmacSha1Options) -> MFKDF2Result { // Validation if let Some(ref id) = options.id && id.is_empty() { return Err(crate::error::MFKDF2Error::MissingFactorId); } - let id = options.id.clone().unwrap_or("hmacsha1".to_string()); + let id = options.id.take().unwrap_or("hmacsha1".to_string()); - let secret = if let Some(secret) = options.secret { + let secret = if let Some(secret) = options.secret.take() { secret } else { let mut secret = [0u8; 20]; @@ -169,7 +164,7 @@ pub fn hmacsha1(options: HmacSha1Options) -> MFKDF2Result { Ok(MFKDF2Factor { id: Some(id), - factor_type: FactorType::HmacSha1(HmacSha1 { padded_secret, response: None, params: None }), + factor_type: FactorType::HmacSha1(HmacSha1 { padded_secret, response: None }), entropy: Some(160.0), }) } diff --git a/mfkdf2/tests/common/mod.rs b/mfkdf2/tests/common/mod.rs index 8e17cc72..4ff12af3 100644 --- a/mfkdf2/tests/common/mod.rs +++ b/mfkdf2/tests/common/mod.rs @@ -262,7 +262,7 @@ pub fn create_derive_factor( .finalize() .into_bytes() .into(); - ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response.into()).unwrap()) + ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response).unwrap()) }, "question" => ("question_1".to_string(), mfkdf2::derive::factors::question("my secret answer").unwrap()), diff --git a/mfkdf2/tests/integration.rs b/mfkdf2/tests/integration.rs index c8751660..f697668d 100644 --- a/mfkdf2/tests/integration.rs +++ b/mfkdf2/tests/integration.rs @@ -139,8 +139,7 @@ fn key_derive_hmacsha1() -> Result<(), mfkdf2::error::MFKDF2Error> { .into_bytes() .into(); - let factor = - ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response.into()).unwrap()); + let factor = ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response).unwrap()); let factors = HashMap::from([factor]); let derived_key = mfkdf2::derive::key(&key.policy, factors, true, false)?; @@ -357,7 +356,7 @@ fn key_derivation_combinations( combo, i ); - policy_for_run = derived_key.policy; + policy_for_run = derived_key.policy.clone(); } } diff --git a/mfkdf2/tests/integrity.rs b/mfkdf2/tests/integrity.rs index ea052642..1eab99f8 100644 --- a/mfkdf2/tests/integrity.rs +++ b/mfkdf2/tests/integrity.rs @@ -68,7 +68,7 @@ fn integrity_enabled_clean_liveness() { // integrity enabled; clean policy should derive and remain stable across runs let setup_derived_key = make_policy(&["password", "hotp", "totp", "uuid"], 4, true); - let policy = setup_derived_key.policy; + let policy = setup_derived_key.policy.clone(); let d1 = derive_once(&policy, &["password", "hotp", "totp", "uuid"], true); let d2 = derive_once(&d1.policy, &["password", "hotp", "totp", "uuid"], true); @@ -81,7 +81,7 @@ fn integrity_enabled_clean_liveness() { fn integrity_enabled_rejects_policy_id_tamper() { let setup_derived_key = make_policy(&["password", "uuid"], 2, true); - let mut policy = setup_derived_key.policy; + let mut policy = setup_derived_key.policy.clone(); policy.id = "tampered".to_string(); @@ -96,7 +96,7 @@ fn integrity_enabled_rejects_policy_id_tamper() { fn integrity_enabled_rejects_threshold_tamper() { let setup_derived_key = make_policy(&["password", "question"], 2, true); - let mut policy = setup_derived_key.policy; + let mut policy = setup_derived_key.policy.clone(); // Tamper threshold policy.threshold += 1; @@ -112,7 +112,7 @@ fn integrity_enabled_rejects_threshold_tamper() { fn integrity_enabled_rejects_salt_tamper() { let setup_derived_key = make_policy(&["password", "totp"], 2, true); - let mut policy = setup_derived_key.policy; + let mut policy = setup_derived_key.policy.clone(); // Tamper salt (base64) policy.salt = "Ny9+L9LQHOKh1x3Acqy7pMb9JaEIfNfxU/TsDON+Ht4=".to_string(); @@ -128,7 +128,7 @@ fn integrity_enabled_rejects_salt_tamper() { fn integrity_enabled_rejects_factor_id_tamper() { let setup_derived_key = make_policy(&["password", "uuid"], 2, true); - let mut policy = setup_derived_key.policy; + let mut policy = setup_derived_key.policy.clone(); // Tamper a factor id (password) if let Some(f) = policy.factors.iter_mut().find(|f| f.id == "password_1") { @@ -156,7 +156,7 @@ fn integrity_enabled_rejects_derived_policy_tamper() { // Start clean with integrity=true let setup_derived_key = make_policy(&["password", "hotp", "totp", "uuid"], 4, true); - let policy = setup_derived_key.policy; + let policy = setup_derived_key.policy.clone(); // First derive succeeds let derived = derive_once(&policy, &["password", "hotp", "uuid", "totp"], true); diff --git a/mfkdf2/tests/stack.rs b/mfkdf2/tests/stack.rs index e1c459f8..7b66593d 100644 --- a/mfkdf2/tests/stack.rs +++ b/mfkdf2/tests/stack.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use hmac::{Hmac, Mac}; -use mfkdf2::{policy::Policy, setup::factors::hmacsha1::HmacSha1Response}; +use mfkdf2::policy::Policy; use sha1::Sha1; const HMACSHA1_SECRET: [u8; 20] = [ @@ -122,10 +122,7 @@ fn stack_derive() { ) .unwrap(), ), - ( - "hmacsha1_1".to_string(), - mfkdf2::derive::factors::hmacsha1(HmacSha1Response::from(response)).unwrap(), - ), + ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response).unwrap()), ])) .unwrap(), )]), From 580bc6022d9e6bd73512fbc685d02c47f1f7a430 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Wed, 3 Dec 2025 14:38:15 +0530 Subject: [PATCH 2/8] add zeroize for policy, derived key, policy_factor --- mfkdf2/src/definitions/bytearray.rs | 59 +++++++++++-------- mfkdf2/src/definitions/entropy.rs | 1 - .../src/definitions/mfkdf_derived_key/mod.rs | 18 ++++++ mfkdf2/src/policy/mod.rs | 37 ++++++++++++ mfkdf2/src/policy/tests.rs | 4 +- 5 files changed, 90 insertions(+), 29 deletions(-) diff --git a/mfkdf2/src/definitions/bytearray.rs b/mfkdf2/src/definitions/bytearray.rs index 704e3d4b..deb045e4 100644 --- a/mfkdf2/src/definitions/bytearray.rs +++ b/mfkdf2/src/definitions/bytearray.rs @@ -43,38 +43,45 @@ impl std::ops::Deref for ByteArray { 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(&self, serializer: S) -> Result + where S: serde::Serializer { + serializer.serialize_newtype_struct(stringify!(ByteArray), &self.0) + } + } -impl Serialize for ByteArray<32> { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - serializer.serialize_newtype_struct("Key", &self.0) - } -} + impl<'de> Deserialize<'de> for ByteArray<$N> { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + struct ByteArrayVisitor; -impl<'de> Deserialize<'de> for ByteArray<32> { - fn deserialize(deserializer: D) -> Result - 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(self, deserializer: D) -> Result + where D: serde::Deserializer<'de> { + let bytes = <[u8; $N]>::deserialize(deserializer)?; + Ok(ByteArray(bytes)) + } + } - fn visit_newtype_struct(self, deserializer: D) -> Result - 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); diff --git a/mfkdf2/src/definitions/entropy.rs b/mfkdf2/src/definitions/entropy.rs index 5bfd6a76..97407c37 100644 --- a/mfkdf2/src/definitions/entropy.rs +++ b/mfkdf2/src/definitions/entropy.rs @@ -9,7 +9,6 @@ /// is not available on subsequent derivations. #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct MFKDF2Entropy { /// Conservative estimate based on how the factor is actually produced or used. Calculated /// using Dropbox's `zxcvbn` estimator. diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs index 698995ad..b0c2178c 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs @@ -68,6 +68,24 @@ impl std::fmt::Display for MFKDF2DerivedKey { } } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for MFKDF2DerivedKey { + fn zeroize(&mut self) { + self.key.zeroize(); + self.secret.zeroize(); + self.shares.zeroize(); + self.policy.zeroize(); + } +} + +#[cfg(feature = "zeroize")] +impl Drop for MFKDF2DerivedKey { + fn drop(&mut self) { + use zeroize::Zeroize; + self.zeroize(); + } +} + impl MFKDF2DerivedKey { /// Derive an internal key for deriving separate keys for parameters, secret, and integrity /// using the policy salt and secret. diff --git a/mfkdf2/src/policy/mod.rs b/mfkdf2/src/policy/mod.rs index 49839962..69a22de4 100644 --- a/mfkdf2/src/policy/mod.rs +++ b/mfkdf2/src/policy/mod.rs @@ -47,6 +47,22 @@ pub struct PolicyFactor { pub hint: Option, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for PolicyFactor { + fn zeroize(&mut self) { + self.salt.zeroize(); + self.secret.zeroize(); + } +} + +#[cfg(feature = "zeroize")] +impl Drop for PolicyFactor { + fn drop(&mut self) { + use zeroize::Zeroize; + self.zeroize(); + } +} + /// MFKDF policy is a set of all allowable factor combinations that can be used to derive the final /// key. MFKDF instance after i-th derivation consists of public construction parameters (threshold, /// salt, etc.), per-factor public parameters (encrypted shares, secret), and factor public state @@ -82,6 +98,27 @@ pub struct Policy { pub key: String, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for Policy { + fn zeroize(&mut self) { + self.salt.zeroize(); + self.factors.zeroize(); + self.hmac.zeroize(); + self.key.zeroize(); + } +} + +#[cfg(feature = "zeroize")] +impl Drop for Policy { + fn drop(&mut self) { + use zeroize::Zeroize; + self.salt.zeroize(); + self.hmac.zeroize(); + self.key.zeroize(); + self.factors.zeroize(); + } +} + impl Policy { /// Returns a list of all factor IDs in the policy. pub fn ids(&self) -> Vec { diff --git a/mfkdf2/src/policy/tests.rs b/mfkdf2/src/policy/tests.rs index 57dcb6c6..aab94911 100644 --- a/mfkdf2/src/policy/tests.rs +++ b/mfkdf2/src/policy/tests.rs @@ -135,7 +135,7 @@ fn create_policy_basic_1() -> policy::Policy { let or2 = or(h1, t1).unwrap(); let policy_factor = and(or1, or2).unwrap(); - policy::setup::setup(policy_factor, PolicySetupOptions::default()).unwrap().policy + policy::setup::setup(policy_factor, PolicySetupOptions::default()).unwrap().policy.clone() } #[test] @@ -205,7 +205,7 @@ fn create_policy_basic_2() -> policy::Policy { let and2 = and(h1, t1).unwrap(); let policy_factor = or(and1, and2).unwrap(); - policy::setup::setup(policy_factor, PolicySetupOptions::default()).unwrap().policy + policy::setup::setup(policy_factor, PolicySetupOptions::default()).unwrap().policy.clone() } #[test] From 34c258c62b5eaac92b1439fd62dd4f22415011a8 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Thu, 4 Dec 2025 00:02:58 +0530 Subject: [PATCH 3/8] add zeroize for individual factors --- mfkdf2/src/derive/factors/hmacsha1.rs | 4 +-- mfkdf2/src/derive/factors/hotp.rs | 8 +++++- mfkdf2/src/derive/factors/ooba.rs | 6 ++-- mfkdf2/src/derive/factors/passkey.rs | 9 +++--- mfkdf2/src/derive/factors/persisted.rs | 3 +- mfkdf2/src/derive/factors/totp.rs | 9 +++++- mfkdf2/src/setup/factors/hmacsha1.rs | 16 +++++++---- mfkdf2/src/setup/factors/hotp.rs | 39 +++++++++++++++++--------- mfkdf2/src/setup/factors/ooba.rs | 15 +++++++--- mfkdf2/src/setup/factors/passkey.rs | 12 +++++--- mfkdf2/src/setup/factors/password.rs | 5 ++-- mfkdf2/src/setup/factors/question.rs | 20 +++++++++---- mfkdf2/src/setup/factors/stack.rs | 24 ++++++++++------ mfkdf2/src/setup/factors/totp.rs | 35 ++++++++++++++--------- mfkdf2/src/setup/factors/uuid.rs | 9 ++++-- 15 files changed, 143 insertions(+), 71 deletions(-) diff --git a/mfkdf2/src/derive/factors/hmacsha1.rs b/mfkdf2/src/derive/factors/hmacsha1.rs index 98472891..91a1fabd 100644 --- a/mfkdf2/src/derive/factors/hmacsha1.rs +++ b/mfkdf2/src/derive/factors/hmacsha1.rs @@ -23,7 +23,7 @@ impl FactorDerive for HmacSha1 { fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> { let response = self.response.as_ref().unwrap(); let mut padded_key = [0u8; 32]; - padded_key[..response.0.len()].copy_from_slice(&response.0); + padded_key[..response.len()].copy_from_slice(&response); let pad = hex::decode( params @@ -117,7 +117,7 @@ impl FactorDerive for HmacSha1 { /// .into(); /// /// // Build the derive‑time HMAC witness and run KeyDerive -/// let derive_factor = derive::factors::hmacsha1(response.into())?; +/// let derive_factor = derive::factors::hmacsha1(response)?; /// let derived_key = derive::key( /// &setup_key.policy, /// HashMap::from([("hmacsha1".to_string(), derive_factor)]), diff --git a/mfkdf2/src/derive/factors/hotp.rs b/mfkdf2/src/derive/factors/hotp.rs index fc025957..f9935aa2 100644 --- a/mfkdf2/src/derive/factors/hotp.rs +++ b/mfkdf2/src/derive/factors/hotp.rs @@ -42,7 +42,7 @@ impl FactorDerive for HOTP { // Decrypt the secret using the factor key let pad = base64::prelude::BASE64_STANDARD.decode(¶ms.pad)?; - let padded_secret = decrypt(pad, &key.0); + let mut padded_secret = decrypt(pad, key); // Generate HOTP code with incremented counter let counter = params.counter + 1; @@ -53,6 +53,12 @@ impl FactorDerive for HOTP { let new_offset = mod_positive(i64::from(self.target) - i64::from(generated_code), 10_i64.pow(params.digits)); + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + padded_secret.zeroize(); + } + Ok(json!({ "hash": params.hash.to_string(), "digits": params.digits, diff --git a/mfkdf2/src/derive/factors/ooba.rs b/mfkdf2/src/derive/factors/ooba.rs index 69865ea7..7290ef04 100644 --- a/mfkdf2/src/derive/factors/ooba.rs +++ b/mfkdf2/src/derive/factors/ooba.rs @@ -27,14 +27,14 @@ impl FactorDerive for Ooba { /// Includes the public parameters for in factor state and decrypts the secret material from /// public parameters. - fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> { + fn include_params(&mut self, mut params: Self::Params) -> MFKDF2Result<()> { let pad_b64 = params["pad"].as_str().ok_or(MFKDF2Error::MissingDeriveParams("pad".to_string()))?; let pad = general_purpose::STANDARD .decode(pad_b64) .map_err(|_| MFKDF2Error::InvalidDeriveParams("pad".to_string()))?; - let config = params["params"].clone(); + let config = params["params"].take(); if !config.is_object() { return Err(MFKDF2Error::InvalidDeriveParams("params".to_string())); } @@ -48,7 +48,7 @@ impl FactorDerive for Ooba { self.length = params["length"] .as_u64() .ok_or(MFKDF2Error::MissingDeriveParams("length".to_string()))? as u8; - let jwk = serde_json::from_value::(params["key"].clone()) + let jwk = serde_json::from_value::(params["key"].take()) .map_err(|_| MFKDF2Error::InvalidDeriveParams("key".to_string()))?; self.jwk = Some(jwk); diff --git a/mfkdf2/src/derive/factors/passkey.rs b/mfkdf2/src/derive/factors/passkey.rs index cd5b1274..59e37763 100644 --- a/mfkdf2/src/derive/factors/passkey.rs +++ b/mfkdf2/src/derive/factors/passkey.rs @@ -8,7 +8,7 @@ use crate::{ definitions::{FactorType, MFKDF2Factor}, derive::FactorDerive, error::MFKDF2Result, - setup::factors::passkey::Passkey, + setup::factors::passkey::{Passkey, PasskeySecret}, }; impl FactorDerive for Passkey { @@ -65,10 +65,10 @@ impl FactorDerive for Passkey { /// assert_eq!(derived_key.key, setup_key.key); /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn passkey(secret: [u8; 32]) -> MFKDF2Result { +pub fn passkey(secret: impl Into) -> MFKDF2Result { Ok(MFKDF2Factor { id: Some("passkey".to_string()), - factor_type: FactorType::Passkey(Passkey { secret: secret.to_vec() }), + factor_type: FactorType::Passkey(Passkey { secret: secret.into() }), entropy: None, }) } @@ -79,6 +79,7 @@ async fn derive_passkey(secret: Vec) -> MFKDF2Result { if secret.len() != 32 { return Err(crate::error::MFKDF2Error::InvalidSecretLength("passkey".to_string())); } + let secret: PasskeySecret = secret.try_into().unwrap(); - passkey(secret.try_into().unwrap()) + passkey(secret) } diff --git a/mfkdf2/src/derive/factors/persisted.rs b/mfkdf2/src/derive/factors/persisted.rs index bb8a04da..fb41992d 100644 --- a/mfkdf2/src/derive/factors/persisted.rs +++ b/mfkdf2/src/derive/factors/persisted.rs @@ -11,8 +11,9 @@ use crate::{ }; /// Persisted share factor state. -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct Persisted { /// Base-64 encoded Shamir share to recover the master secret. pub share: Vec, diff --git a/mfkdf2/src/derive/factors/totp.rs b/mfkdf2/src/derive/factors/totp.rs index ff6c1269..b8a564dc 100644 --- a/mfkdf2/src/derive/factors/totp.rs +++ b/mfkdf2/src/derive/factors/totp.rs @@ -102,7 +102,7 @@ impl FactorDerive for TOTP { let params: TOTPParams = serde_json::from_value(self.params.clone())?; let pad = base64::prelude::BASE64_STANDARD.decode(params.pad)?; - let padded_secret = decrypt(pad.clone(), &key.0); + let mut padded_secret = decrypt(pad.clone(), key.as_ref()); let time = params.start; let mut new_offsets = Vec::with_capacity((4 * params.window) as usize); @@ -138,6 +138,13 @@ impl FactorDerive for TOTP { offsets: base64::prelude::BASE64_STANDARD.encode(&new_offsets), }; + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + padded_secret.zeroize(); + new_offsets.zeroize(); + } + Ok(serde_json::to_value(params)?) } } diff --git a/mfkdf2/src/setup/factors/hmacsha1.rs b/mfkdf2/src/setup/factors/hmacsha1.rs index c9088e6d..d450b3a6 100644 --- a/mfkdf2/src/setup/factors/hmacsha1.rs +++ b/mfkdf2/src/setup/factors/hmacsha1.rs @@ -33,7 +33,6 @@ use crate::{ /// and the token agree on the same `kₜ`. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct HmacSha1Options { /// Optional application-defined identifier for the factor. Defaults to `"hmacsha1"`. If /// provided, it must be non-empty. @@ -56,7 +55,7 @@ pub(crate) type HmacSha1Response = ByteArray<20>; /// that the derive side can use to confirm it has the same secret. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct HmacSha1 { /// HMAC‑SHA1 response pub response: Option, @@ -83,6 +82,12 @@ impl FactorSetup for HmacSha1 { padded_key[..response.len()].copy_from_slice(&response); let pad = encrypt(&self.padded_secret, &padded_key); + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + padded_key.zeroize(); + } + Ok(json!({ "challenge": hex::encode(challenge), "pad": hex::encode(pad), @@ -148,13 +153,12 @@ pub fn hmacsha1(mut options: HmacSha1Options) -> MFKDF2Result { } let id = options.id.take().unwrap_or("hmacsha1".to_string()); - let secret = if let Some(secret) = options.secret.take() { - secret - } else { + // consume the secret from options and generate a random one if none is provided + let secret = options.secret.take().unwrap_or_else(|| { let mut secret = [0u8; 20]; rng::fill_bytes(&mut secret); secret.to_vec() - }; + }); if secret.len() != 20 { return Err(crate::error::MFKDF2Error::InvalidSecretLength(id)); } diff --git a/mfkdf2/src/setup/factors/hotp.rs b/mfkdf2/src/setup/factors/hotp.rs index a7407523..70de1bd0 100644 --- a/mfkdf2/src/setup/factors/hotp.rs +++ b/mfkdf2/src/setup/factors/hotp.rs @@ -99,17 +99,22 @@ pub struct HOTPConfig { pub label: String, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for HOTPConfig { + fn zeroize(&mut self) { self.secret.zeroize(); } +} + impl TryFrom for HOTPConfig { type Error = MFKDF2Error; - fn try_from(value: HOTPOptions) -> Result { + fn try_from(mut value: HOTPOptions) -> Result { Ok(HOTPConfig { - id: value.id.ok_or(MFKDF2Error::MissingFactorId)?, - secret: value.secret.ok_or(MFKDF2Error::MissingSetupParams("secret".to_string()))?, + id: value.id.take().ok_or(MFKDF2Error::MissingFactorId)?, + secret: value.secret.take().ok_or(MFKDF2Error::MissingSetupParams("secret".to_string()))?, digits: value.digits.ok_or(MFKDF2Error::InvalidHOTPDigits)?, - hash: value.hash.unwrap_or(HashAlgorithm::Sha1), - issuer: value.issuer.unwrap_or("MFKDF".to_string()), - label: value.label.unwrap_or("mfkdf.com".to_string()), + hash: value.hash.take().unwrap_or(HashAlgorithm::Sha1), + issuer: value.issuer.take().unwrap_or("MFKDF".to_string()), + label: value.label.take().unwrap_or("mfkdf.com".to_string()), }) } } @@ -138,10 +143,19 @@ pub struct HOTP { pub params: Value, /// HOTP code. pub code: u32, - /// HOTP factor material. The target code that is used to derive the key. + /// HOTP secret factor material. The target code that is used to derive the key. pub target: u32, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for HOTP { + fn zeroize(&mut self) { + self.config.zeroize(); + self.target.zeroize(); + self.code.zeroize(); + } +} + /// HOTP public parameters. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] @@ -177,7 +191,7 @@ impl FactorSetup for HOTP { let offset = mod_positive(i64::from(self.target) - i64::from(code), 10_i64.pow(self.config.digits)); - let pad = encrypt(&self.config.secret, &key.0); + let pad = encrypt(&self.config.secret, key.as_ref()); let params = HOTPParams { hash: self.config.hash.clone(), @@ -265,16 +279,14 @@ pub fn mod_positive(n: i64, m: i64) -> u32 { (((n % m) + m) % m) as u32 } /// assert!(matches!(result, Err(mfkdf2::error::MFKDF2Error::InvalidHOTPDigits))); /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn hotp(options: HOTPOptions) -> MFKDF2Result { - let mut options = options; - +pub fn hotp(mut options: HOTPOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id && id.is_empty() { return Err(crate::error::MFKDF2Error::MissingFactorId); } - let id = options.id.clone().unwrap_or("hotp".to_string()); + let id = options.id.take().unwrap_or("hotp".to_string()); if let Some(digits) = options.digits && !(6..=8).contains(&digits) @@ -290,7 +302,8 @@ pub fn hotp(options: HOTPOptions) -> MFKDF2Result { { return Err(crate::error::MFKDF2Error::InvalidSecretLength(id)); } - let secret = options.secret.unwrap_or_else(|| { + // consume the secret from options and generate a random one if none is provided + let secret = options.secret.take().unwrap_or_else(|| { let mut secret = [0u8; 20]; crate::rng::fill_bytes(&mut secret); secret.to_vec() diff --git a/mfkdf2/src/setup/factors/ooba.rs b/mfkdf2/src/setup/factors/ooba.rs index 7280b751..29174bfd 100644 --- a/mfkdf2/src/setup/factors/ooba.rs +++ b/mfkdf2/src/setup/factors/ooba.rs @@ -100,6 +100,13 @@ pub struct Ooba { pub params: Value, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for Ooba { + fn zeroize(&mut self) { + self.target.zeroize(); + self.code.zeroize(); + } +} impl TryFrom<&Jwk> for OobaPublicKey { type Error = MFKDF2Error; @@ -218,7 +225,7 @@ impl FactorSetup for Ooba { /// assert!(matches!(result, Err(MFKDF2Error::InvalidOobaLength))); /// # Ok::<(), MFKDF2Error>(()) /// ``` -pub fn ooba(options: OobaOptions) -> MFKDF2Result { +pub fn ooba(mut options: OobaOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id && id.is_empty() @@ -230,8 +237,8 @@ pub fn ooba(options: OobaOptions) -> MFKDF2Result { return Err(MFKDF2Error::InvalidOobaLength); } - let params = options.params.unwrap_or(json!({})); - let key = options.key.ok_or(MFKDF2Error::MissingOobaKey)?; + let params = options.params.take().unwrap_or_default(); + let key = options.key.take().ok_or(MFKDF2Error::MissingOobaKey)?; // verify that key is rsa public key if !matches!(key.algorithm, jsonwebtoken::jwk::AlgorithmParameters::RSA(_)) { return Err(MFKDF2Error::InvalidOobaKey); @@ -242,7 +249,7 @@ pub fn ooba(options: OobaOptions) -> MFKDF2Result { crate::rng::fill_bytes(&mut target); Ok(MFKDF2Factor { - id: Some(options.id.unwrap_or("ooba".to_string())), + id: Some(options.id.take().unwrap_or("ooba".to_string())), factor_type: FactorType::OOBA(Ooba { target: target.to_vec(), length, diff --git a/mfkdf2/src/setup/factors/passkey.rs b/mfkdf2/src/setup/factors/passkey.rs index e68e3963..e8baf4c4 100644 --- a/mfkdf2/src/setup/factors/passkey.rs +++ b/mfkdf2/src/setup/factors/passkey.rs @@ -22,24 +22,28 @@ use serde::{Deserialize, Serialize}; use crate::{ - definitions::{FactorType, MFKDF2Factor, factor::FactorMetadata}, + definitions::{ByteArray, FactorType, MFKDF2Factor, factor::FactorMetadata}, error::MFKDF2Result, setup::FactorSetup, }; +/// 32 byte passkey secret +pub type PasskeySecret = ByteArray<32>; + /// Passkey factor state #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct Passkey { /// 32‑byte secret derived from the passkey’s `WebAuthn` PRF output or equivalent /// hardware‑protected key - pub secret: Vec, + pub secret: PasskeySecret, } impl FactorMetadata for Passkey { fn kind(&self) -> String { "passkey".to_string() } - fn bytes(&self) -> Vec { self.secret.clone() } + fn bytes(&self) -> Vec { self.secret.to_vec() } } impl FactorSetup for Passkey { @@ -98,7 +102,7 @@ pub fn passkey(secret: [u8; 32], options: PasskeyOptions) -> MFKDF2Result::into(password); + let password = password.into(); if password.is_empty() { return Err(MFKDF2Error::PasswordEmpty); } diff --git a/mfkdf2/src/setup/factors/question.rs b/mfkdf2/src/setup/factors/question.rs index 752aa7e1..82d597c7 100644 --- a/mfkdf2/src/setup/factors/question.rs +++ b/mfkdf2/src/setup/factors/question.rs @@ -41,6 +41,14 @@ pub struct Question { pub question: String, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for Question { + fn zeroize(&mut self) { + self.answer.zeroize(); + self.question.zeroize(); + } +} + impl FactorMetadata for Question { fn kind(&self) -> String { "question".to_string() } @@ -86,7 +94,10 @@ impl FactorSetup for Question { /// assert_eq!(factor.id.as_deref(), Some("recovery-question")); /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn question(answer: impl Into, options: QuestionOptions) -> MFKDF2Result { +pub fn question( + answer: impl Into, + mut options: QuestionOptions, +) -> MFKDF2Result { let answer = answer.into(); if answer.is_empty() { return Err(MFKDF2Error::AnswerEmpty); @@ -98,12 +109,9 @@ pub fn question(answer: impl Into, options: QuestionOptions) -> MFKDF2Re { return Err(crate::error::MFKDF2Error::MissingFactorId); } - let id = Some(options.id.clone().unwrap_or("question".to_string())); + let id = Some(options.id.take().unwrap_or("question".to_string())); - let question = match options.question { - None => String::new(), - Some(ref ques) => ques.clone(), - }; + let question = options.question.take().unwrap_or_default(); let answer = answer .to_lowercase() diff --git a/mfkdf2/src/setup/factors/stack.rs b/mfkdf2/src/setup/factors/stack.rs index 5200c40e..e7d98b81 100644 --- a/mfkdf2/src/setup/factors/stack.rs +++ b/mfkdf2/src/setup/factors/stack.rs @@ -30,17 +30,15 @@ pub struct StackOptions { } impl From for MFKDF2Options { - fn from(value: StackOptions) -> Self { - let StackOptions { id, threshold, salt } = value; - + fn from(mut value: StackOptions) -> Self { MFKDF2Options { - id, - threshold, - salt, - stack: Some(true), + id: value.id.take(), + threshold: value.threshold.take(), + salt: value.salt.take(), + stack: Some(true), integrity: Some(false), - time: None, - memory: None, + time: None, + memory: None, } } } @@ -58,6 +56,14 @@ pub struct Stack { pub key: MFKDF2DerivedKey, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for Stack { + fn zeroize(&mut self) { + self.factors.values_mut().for_each(|factor| factor.zeroize()); + self.key.zeroize(); + } +} + impl FactorMetadata for Stack { fn kind(&self) -> String { "stack".to_string() } diff --git a/mfkdf2/src/setup/factors/totp.rs b/mfkdf2/src/setup/factors/totp.rs index 18ad09e8..51c93356 100644 --- a/mfkdf2/src/setup/factors/totp.rs +++ b/mfkdf2/src/setup/factors/totp.rs @@ -81,7 +81,7 @@ pub struct TOTPOptions { pub window: Option, /// Step size in seconds (the TOTP "period", default 30s) pub step: Option, - /// Optional per‑time overrides for debugging or advanced flows + /// Optional timing oracle to harden TOTP factor construction pub oracle: Option>, } @@ -132,6 +132,11 @@ pub struct TOTPConfig { pub oracle: Option>, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for TOTPConfig { + fn zeroize(&mut self) { self.secret.zeroize(); } +} + impl TryFrom for TOTPConfig { type Error = MFKDF2Error; @@ -205,6 +210,15 @@ pub struct TOTP { pub target: u32, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for TOTP { + fn zeroize(&mut self) { + self.config.zeroize(); + self.target.zeroize(); + self.code.zeroize(); + } +} + impl FactorMetadata for TOTP { fn kind(&self) -> String { "totp".to_string() } @@ -249,7 +263,7 @@ impl FactorSetup for TOTP { offsets.extend_from_slice(&offset.to_be_bytes()); } - let pad = encrypt(&self.config.secret, &key.0); + let pad = encrypt(&self.config.secret, &key); let params = TOTPParams { start: time as u64, @@ -331,16 +345,14 @@ impl FactorSetup for TOTP { /// assert!(matches!(result, Err(MFKDF2Error::InvalidSecretLength(_)))); /// # Ok::<(), MFKDF2Error>(()) /// ``` -pub fn totp(options: TOTPOptions) -> MFKDF2Result { - let mut options = options; - +pub fn totp(mut options: TOTPOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id && id.is_empty() { return Err(crate::error::MFKDF2Error::MissingFactorId); } - let id = options.id.clone().unwrap_or("totp".to_string()); + let id = options.id.take().unwrap_or("totp".to_string()); if let Some(digits) = options.digits && !(6..=8).contains(&digits) @@ -349,17 +361,14 @@ pub fn totp(options: TOTPOptions) -> MFKDF2Result { } options.digits = Some(options.digits.unwrap_or(6)); - // secret length validation - if let Some(ref secret) = options.secret - && secret.len() != 20 - { - return Err(crate::error::MFKDF2Error::InvalidSecretLength(id)); - } - let secret = options.secret.unwrap_or_else(|| { + let secret = options.secret.take().unwrap_or_else(|| { let mut secret = [0u8; 20]; crate::rng::fill_bytes(&mut secret); secret.to_vec() }); + if secret.len() != 20 { + return Err(crate::error::MFKDF2Error::InvalidSecretLength(id)); + } if options.time.is_none() { let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; diff --git a/mfkdf2/src/setup/factors/uuid.rs b/mfkdf2/src/setup/factors/uuid.rs index e67d69d5..7fa5c3ee 100644 --- a/mfkdf2/src/setup/factors/uuid.rs +++ b/mfkdf2/src/setup/factors/uuid.rs @@ -36,6 +36,11 @@ pub struct UUIDFactor { pub uuid: Uuid, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for UUIDFactor { + fn zeroize(&mut self) {} +} + impl FactorMetadata for UUIDFactor { fn kind(&self) -> String { "uuid".to_string() } @@ -71,7 +76,7 @@ impl FactorSetup for UUIDFactor { /// assert_eq!(factor.id.as_deref(), Some("uuid")); /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) /// ``` -pub fn uuid(options: UUIDOptions) -> MFKDF2Result { +pub fn uuid(mut options: UUIDOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id && id.is_empty() @@ -79,7 +84,7 @@ pub fn uuid(options: UUIDOptions) -> MFKDF2Result { return Err(crate::error::MFKDF2Error::MissingFactorId); } - let uuid = options.uuid.unwrap_or(Uuid::new_v4()); + let uuid = options.uuid.take().unwrap_or(Uuid::new_v4()); Ok(MFKDF2Factor { id: Some(options.id.unwrap_or("uuid".to_string())), From ecbbbbfd9fbdd17221a54b5d7acb7cc39d41a482 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Thu, 4 Dec 2025 00:03:28 +0530 Subject: [PATCH 4/8] zeroize: derived key --- .../definitions/mfkdf_derived_key/crypto.rs | 12 +++++-- .../definitions/mfkdf_derived_key/hints.rs | 22 ++++++++++--- .../src/definitions/mfkdf_derived_key/mod.rs | 32 +++++++++++-------- .../mfkdf_derived_key/reconstitution.rs | 16 ++++++++-- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs b/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs index 27e1fa56..6ec01a6b 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs @@ -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) } } diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs b/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs index 3eeddd6b..0a401457 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs @@ -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 @@ -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, @@ -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() @@ -143,7 +151,7 @@ impl MFKDF2DerivedKey { /// ``` pub fn add_hint(&mut self, factor_id: &str, bits: Option) -> 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() { @@ -175,6 +183,12 @@ impl MFKDF2DerivedKey { self.policy.hmac = hmac; } + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + internal_key.zeroize(); + } + Ok(()) } } diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs index b0c2178c..d6201d98 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs @@ -18,7 +18,10 @@ use serde_json::Value; use crate::{ crypto::decrypt, - definitions::{bytearray::Key, entropy::MFKDF2Entropy}, + definitions::{ + bytearray::{ByteArray, Key}, + entropy::MFKDF2Entropy, + }, error::MFKDF2Result, policy::Policy, }; @@ -30,6 +33,9 @@ mod persistence; pub mod reconstitution; mod strengthening; +/// Internal secret material that is split into per‑factor shares for threshold recovery +pub type MFKDF2DerivedSecret = ByteArray<32>; + /// MFKDF2 Derived key after the setup or derive operation. /// /// An [`MFKDF2DerivedKey`] bundles the static derived key material together with the resolved @@ -45,7 +51,7 @@ pub struct MFKDF2DerivedKey { /// Final 32‑byte key output of the KDF pub key: Key, /// Internal secret material that is split into per‑factor shares for threshold recovery - pub secret: Vec, + pub secret: MFKDF2DerivedSecret, /// Shamir‑style shares of `secret`, one per factor, used by reconstitution and /// threshold‑management routines. pub shares: Vec>, @@ -63,7 +69,7 @@ impl std::fmt::Display for MFKDF2DerivedKey { f, "MFKDF2DerivedKey {{ key: {}, secret: {} }}", general_purpose::STANDARD.encode(self.key.as_ref()), - general_purpose::STANDARD.encode(self.secret.clone()), + general_purpose::STANDARD.encode(self.secret.as_ref()), ) } } @@ -78,14 +84,6 @@ impl zeroize::Zeroize for MFKDF2DerivedKey { } } -#[cfg(feature = "zeroize")] -impl Drop for MFKDF2DerivedKey { - fn drop(&mut self) { - use zeroize::Zeroize; - self.zeroize(); - } -} - impl MFKDF2DerivedKey { /// Derive an internal key for deriving separate keys for parameters, secret, and integrity /// using the policy salt and secret. @@ -105,8 +103,16 @@ impl MFKDF2DerivedKey { ) .hash_password_into(&self.secret, &salt, &mut kek)?; - let policy_key = general_purpose::STANDARD.decode(&self.policy.key)?; - let key = decrypt(policy_key, &kek); + let mut policy_key = general_purpose::STANDARD.decode(&self.policy.key)?; + let key = decrypt(policy_key.clone(), &kek); + + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + kek.zeroize(); + policy_key.zeroize(); + } + key.try_into() } } diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs b/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs index b1156af3..c928239b 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs @@ -163,7 +163,7 @@ impl MFKDF2DerivedKey { let threshold = threshold.unwrap_or(self.policy.threshold); // derive internal key for deriving separate keys for parameters, secret, and integrity - let internal_key = self.derive_internal_key()?; + let mut internal_key = self.derive_internal_key()?; for factor in &self.policy.factors { factors.insert(factor.id.clone(), factor.clone()); @@ -261,7 +261,7 @@ impl MFKDF2DerivedKey { return Err(MFKDF2Error::TryFromVec); }; - let secret_key = hkdf_sha256_with_info( + let mut secret_key = hkdf_sha256_with_info( &internal_key, &salt, format!("mfkdf2:factor:secret:{}", factor.id).as_bytes(), @@ -271,6 +271,12 @@ impl MFKDF2DerivedKey { factor.secret = general_purpose::STANDARD.encode(encrypt(stretched, &secret_key)); new_factors.push(factor); + + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + secret_key.zeroize(); + } } self.policy.factors = new_factors; @@ -285,6 +291,12 @@ impl MFKDF2DerivedKey { let integrity_data = self.policy.extract(); let digest = hmacsha256(&integrity_key, &integrity_data); self.policy.hmac = general_purpose::STANDARD.encode(digest); + + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + internal_key.zeroize(); + } } Ok(()) From 2a2b4accb315809d7fe272f6fabe844979cdafff Mon Sep 17 00:00:00 2001 From: lonerapier Date: Thu, 4 Dec 2025 00:06:01 +0530 Subject: [PATCH 5/8] bytearray and otpauth --- mfkdf2/src/crypto.rs | 8 ++++---- mfkdf2/src/definitions/bytearray.rs | 4 ++-- mfkdf2/src/definitions/factor.rs | 4 +++- mfkdf2/src/definitions/uniffi_types.rs | 2 +- mfkdf2/src/otpauth.rs | 13 +++++++++++++ mfkdf2/src/policy/mod.rs | 19 ------------------- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/mfkdf2/src/crypto.rs b/mfkdf2/src/crypto.rs index c9cbd759..f212f06a 100644 --- a/mfkdf2/src/crypto.rs +++ b/mfkdf2/src/crypto.rs @@ -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 { +pub(crate) fn encrypt(data: &[u8], key: impl AsRef<[u8]>) -> Vec { // Ensure the input is a multiple of 16 by zero-padding if necessary. let mut buf = { let mut v = data.to_vec(); @@ -31,7 +31,7 @@ pub(crate) fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec { v }; - let cipher = Encryptor::::new_from_slice(key).expect("Invalid AES-256 key"); + let cipher = Encryptor::::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::(&mut buf, padded_len).expect("ECB encryption"); buf @@ -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, key: &[u8; 32]) -> Vec { - let cipher = Decryptor::::new_from_slice(key).expect("Invalid AES key"); +pub(crate) fn decrypt(mut data: Vec, key: impl AsRef<[u8]>) -> Vec { + let cipher = Decryptor::::new_from_slice(key.as_ref()).expect("Invalid AES key"); let _ = cipher.decrypt_padded_mut::(&mut data).expect("ECB decrypt"); data } diff --git a/mfkdf2/src/definitions/bytearray.rs b/mfkdf2/src/definitions/bytearray.rs index deb045e4..03302b19 100644 --- a/mfkdf2/src/definitions/bytearray.rs +++ b/mfkdf2/src/definitions/bytearray.rs @@ -3,9 +3,9 @@ use serde::{Deserialize, Serialize}; /// Generic fixed-size byte array used as the basis for key-like types. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] -pub struct ByteArray(pub [u8; N]); +pub struct ByteArray([u8; N]); /// 32 byte key pub type Key = ByteArray<32>; diff --git a/mfkdf2/src/definitions/factor.rs b/mfkdf2/src/definitions/factor.rs index 81351524..505139ff 100644 --- a/mfkdf2/src/definitions/factor.rs +++ b/mfkdf2/src/definitions/factor.rs @@ -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, @@ -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), diff --git a/mfkdf2/src/definitions/uniffi_types.rs b/mfkdf2/src/definitions/uniffi_types.rs index 278733ed..fea474a2 100644 --- a/mfkdf2/src/definitions/uniffi_types.rs +++ b/mfkdf2/src/definitions/uniffi_types.rs @@ -15,7 +15,7 @@ uniffi::custom_type!(HmacSha1Response, Vec, { if v.len() == 20 { let mut arr = [0u8; 20]; arr.copy_from_slice(&v); - Ok(HmacSha1Response(arr)) + Ok(arr.into()) } else { Err(uniffi::deps::anyhow::anyhow!( "Expected Vec of length 20, got {}", diff --git a/mfkdf2/src/otpauth.rs b/mfkdf2/src/otpauth.rs index d6e94e52..fc648b41 100644 --- a/mfkdf2/src/otpauth.rs +++ b/mfkdf2/src/otpauth.rs @@ -93,6 +93,19 @@ pub struct OtpAuthUrlOptions { pub algorithm: Option, } +#[cfg(feature = "zeroize")] +impl zeroize::Zeroize for OtpAuthUrlOptions { + fn zeroize(&mut self) { self.secret.zeroize(); } +} + +#[cfg(feature = "zeroize")] +impl Drop for OtpAuthUrlOptions { + fn drop(&mut self) { + use zeroize::Zeroize; + self.zeroize(); + } +} + /// Convert an input secret (with a declared encoding) into Base32 (no padding), /// removing spaces and normalizing case where needed. fn secret_to_base32_no_pad(secret: &str, enc: &Encoding) -> Result { diff --git a/mfkdf2/src/policy/mod.rs b/mfkdf2/src/policy/mod.rs index 69a22de4..1d3ec3b8 100644 --- a/mfkdf2/src/policy/mod.rs +++ b/mfkdf2/src/policy/mod.rs @@ -55,14 +55,6 @@ impl zeroize::Zeroize for PolicyFactor { } } -#[cfg(feature = "zeroize")] -impl Drop for PolicyFactor { - fn drop(&mut self) { - use zeroize::Zeroize; - self.zeroize(); - } -} - /// MFKDF policy is a set of all allowable factor combinations that can be used to derive the final /// key. MFKDF instance after i-th derivation consists of public construction parameters (threshold, /// salt, etc.), per-factor public parameters (encrypted shares, secret), and factor public state @@ -108,17 +100,6 @@ impl zeroize::Zeroize for Policy { } } -#[cfg(feature = "zeroize")] -impl Drop for Policy { - fn drop(&mut self) { - use zeroize::Zeroize; - self.salt.zeroize(); - self.hmac.zeroize(); - self.key.zeroize(); - self.factors.zeroize(); - } -} - impl Policy { /// Returns a list of all factor IDs in the policy. pub fn ids(&self) -> Vec { From db8c2bc53fe7dd73e2d8b052902090532a46ff91 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Thu, 4 Dec 2025 00:06:58 +0530 Subject: [PATCH 6/8] add zeroize for setup and derive --- mfkdf2/README.md | 2 +- mfkdf2/src/derive/key.rs | 36 ++++++++++++++++++++++----------- mfkdf2/src/setup/key.rs | 43 ++++++++++++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/mfkdf2/README.md b/mfkdf2/README.md index 6da2e374..cdb72c8c 100644 --- a/mfkdf2/README.md +++ b/mfkdf2/README.md @@ -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 diff --git a/mfkdf2/src/derive/key.rs b/mfkdf2/src/derive/key.rs index 2dcfc819..274b65c7 100644 --- a/mfkdf2/src/derive/key.rs +++ b/mfkdf2/src/derive/key.rs @@ -93,7 +93,7 @@ use crate::{ /// .finalize() /// .into_bytes() /// .into(); -/// let derive_hmac = derive_hmacsha1(response.into())?; +/// let derive_hmac = derive_hmacsha1(response)?; /// /// let derived_key = derive::key( /// &setup_key.policy, @@ -320,15 +320,18 @@ pub fn key( let sss = SecretSharing(policy.threshold); let secret = sss.recover(&shares_vec).map_err(|_| MFKDF2Error::ShareRecovery)?; - let secret_arr: [u8; 32] = secret[..32].try_into().map_err(|_| MFKDF2Error::TryFromVec)?; + let mut secret_arr: [u8; 32] = secret.try_into().map_err(|_| MFKDF2Error::TryFromVec)?; let salt_bytes = general_purpose::STANDARD.decode(&policy.salt)?; // Generate key let mut kek = [0u8; 32]; if stack { // stack key - kek = - hkdf_sha256_with_info(&secret, &salt_bytes, format!("mfkdf2:stack:{}", policy.id).as_bytes()); + kek = hkdf_sha256_with_info( + &secret_arr, + &salt_bytes, + format!("mfkdf2:stack:{}", policy.id).as_bytes(), + ); } else { // default key Argon2::new( @@ -344,13 +347,12 @@ pub fn key( .hash_password_into(&secret_arr, &salt_bytes, &mut kek)?; } - let policy_key_bytes = general_purpose::STANDARD.decode(policy.key.as_bytes())?; - // Create an internal key for deriving separate keys for parameters, secret, and integrity - let internal_key = decrypt(policy_key_bytes.clone(), &kek); + let policy_key_bytes = general_purpose::STANDARD.decode(policy.key.as_bytes())?; + let internal_key = decrypt(policy_key_bytes, &kek); // Perform integrity check - let integrity_key = + let mut integrity_key = hkdf_sha256_with_info(&internal_key, &salt_bytes, "mfkdf2:integrity".as_bytes()); if verify { let integrity_data = policy.extract(); @@ -426,20 +428,30 @@ pub fn key( .map_err(|_| MFKDF2Error::ShareRecovery)?; // derive a dedicated final key to ensure domain separation between internal and external keys - let final_key = if !stack { + let final_key: [u8; 32] = if !stack { hkdf_sha256_with_info(&internal_key, &salt_bytes, "mfkdf2:key:final".as_bytes()) } else { internal_key.try_into().map_err(|_| MFKDF2Error::TryFromVec)? }; - Ok(MFKDF2DerivedKey { + let result = MFKDF2DerivedKey { policy: new_policy, key: final_key.into(), - secret: secret_arr.to_vec(), + secret: secret_arr.into(), shares: original_shares.into_iter().map(|s| Vec::from(&s)).collect(), outputs, entropy: MFKDF2Entropy { real: 0.0, theoretical: 0 }, - }) + }; + + #[cfg(feature = "zeroize")] + { + use zeroize::Zeroize; + secret_arr.zeroize(); + kek.zeroize(); + integrity_key.zeroize(); + } + + Ok(result) } #[cfg(feature = "bindings")] diff --git a/mfkdf2/src/setup/key.rs b/mfkdf2/src/setup/key.rs index 81ccc1d2..b1b86f07 100644 --- a/mfkdf2/src/setup/key.rs +++ b/mfkdf2/src/setup/key.rs @@ -268,18 +268,18 @@ pub fn key(factors: &[MFKDF2Factor], options: MFKDF2Options) -> MFKDF2Result MFKDF2Result MFKDF2Result Date: Thu, 4 Dec 2025 00:27:07 +0530 Subject: [PATCH 7/8] fix: uniffi types --- mfkdf2/src/definitions/uniffi_types.rs | 2 +- mfkdf2/src/derive/key.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mfkdf2/src/definitions/uniffi_types.rs b/mfkdf2/src/definitions/uniffi_types.rs index fea474a2..38576a3e 100644 --- a/mfkdf2/src/definitions/uniffi_types.rs +++ b/mfkdf2/src/definitions/uniffi_types.rs @@ -10,7 +10,7 @@ use crate::{ }; uniffi::custom_type!(HmacSha1Response, Vec, { - lower: |r| r.0.to_vec(), + lower: |r| r.to_vec(), try_lift: |v: Vec| { if v.len() == 20 { let mut arr = [0u8; 20]; diff --git a/mfkdf2/src/derive/key.rs b/mfkdf2/src/derive/key.rs index 274b65c7..f4f74d89 100644 --- a/mfkdf2/src/derive/key.rs +++ b/mfkdf2/src/derive/key.rs @@ -320,7 +320,7 @@ pub fn key( let sss = SecretSharing(policy.threshold); let secret = sss.recover(&shares_vec).map_err(|_| MFKDF2Error::ShareRecovery)?; - let mut secret_arr: [u8; 32] = secret.try_into().map_err(|_| MFKDF2Error::TryFromVec)?; + let mut secret_arr: [u8; 32] = secret[..32].try_into().map_err(|_| MFKDF2Error::TryFromVec)?; let salt_bytes = general_purpose::STANDARD.decode(&policy.salt)?; // Generate key From e82c566ba9edcd660d246b68620a2a0b1160f552 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Thu, 4 Dec 2025 00:35:23 +0530 Subject: [PATCH 8/8] suppress unused mut warning for not zeroize feature --- mfkdf2/src/definitions/mfkdf_derived_key/mod.rs | 1 + mfkdf2/src/derive/factors/hotp.rs | 1 + mfkdf2/src/derive/factors/totp.rs | 1 + mfkdf2/src/derive/key.rs | 2 ++ mfkdf2/src/setup/key.rs | 2 ++ 5 files changed, 7 insertions(+) diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs index d6201d98..40721328 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs @@ -9,6 +9,7 @@ //! key, while the embedded [`Policy`] and threshold secret-sharing scheme enable flexible recovery //! flows (such as `t`‑of‑`n` factor policies) without weakening the guarantees provided by the //! underlying multi-factor construction. +#![cfg_attr(not(feature = "zeroize"), allow(unused_mut))] use std::collections::HashMap; use argon2::Argon2; diff --git a/mfkdf2/src/derive/factors/hotp.rs b/mfkdf2/src/derive/factors/hotp.rs index f9935aa2..89e10478 100644 --- a/mfkdf2/src/derive/factors/hotp.rs +++ b/mfkdf2/src/derive/factors/hotp.rs @@ -42,6 +42,7 @@ impl FactorDerive for HOTP { // Decrypt the secret using the factor key let pad = base64::prelude::BASE64_STANDARD.decode(¶ms.pad)?; + #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))] let mut padded_secret = decrypt(pad, key); // Generate HOTP code with incremented counter diff --git a/mfkdf2/src/derive/factors/totp.rs b/mfkdf2/src/derive/factors/totp.rs index b8a564dc..c205e1c8 100644 --- a/mfkdf2/src/derive/factors/totp.rs +++ b/mfkdf2/src/derive/factors/totp.rs @@ -102,6 +102,7 @@ impl FactorDerive for TOTP { let params: TOTPParams = serde_json::from_value(self.params.clone())?; let pad = base64::prelude::BASE64_STANDARD.decode(params.pad)?; + #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))] let mut padded_secret = decrypt(pad.clone(), key.as_ref()); let time = params.start; diff --git a/mfkdf2/src/derive/key.rs b/mfkdf2/src/derive/key.rs index f4f74d89..6a8dea25 100644 --- a/mfkdf2/src/derive/key.rs +++ b/mfkdf2/src/derive/key.rs @@ -320,6 +320,7 @@ pub fn key( let sss = SecretSharing(policy.threshold); let secret = sss.recover(&shares_vec).map_err(|_| MFKDF2Error::ShareRecovery)?; + #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))] let mut secret_arr: [u8; 32] = secret[..32].try_into().map_err(|_| MFKDF2Error::TryFromVec)?; let salt_bytes = general_purpose::STANDARD.decode(&policy.salt)?; @@ -352,6 +353,7 @@ pub fn key( let internal_key = decrypt(policy_key_bytes, &kek); // Perform integrity check + #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))] let mut integrity_key = hkdf_sha256_with_info(&internal_key, &salt_bytes, "mfkdf2:integrity".as_bytes()); if verify { diff --git a/mfkdf2/src/setup/key.rs b/mfkdf2/src/setup/key.rs index b1b86f07..c443accf 100644 --- a/mfkdf2/src/setup/key.rs +++ b/mfkdf2/src/setup/key.rs @@ -4,6 +4,8 @@ //! //! Master secret `M` is split into Shamir shares `Sᵢ` over the configured polynomial, and encrypted //! to produce encrypted shares `Cᵢ` which is then stored in the [`Policy`]. + +#![cfg_attr(not(feature = "zeroize"), allow(unused_mut))] // TODO (autoparallel): If we use `no-std`, then this use of `HashSet` will need to be // replaced. use std::collections::{HashMap, HashSet};