Skip to content

Commit 66f659a

Browse files
committed
docs: derive factors
1 parent 36acc2a commit 66f659a

19 files changed

Lines changed: 660 additions & 104 deletions

File tree

mfkdf2/src/derive/factors/hmacsha1.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
//! Factor construction derive phase for the HMAC‑SHA1 factor from
2+
//! [HMAC-SHA1](`crate::setup::factors::hmacsha1`).
3+
//!
4+
//! - During setup, the factor stores a padded HMAC key and a challenge in the policy.
5+
//! - During derive, this module consumes an HMAC response over that challenge and reconstructs the
6+
//! same padded secret so that the factor contributes identical bytes to the MFKDF2 key
7+
//! derivation.
18
use serde_json::{Value, json};
29

310
use crate::{
@@ -12,6 +19,7 @@ impl FactorDerive for HmacSha1 {
1219
type Output = Value;
1320
type Params = Value;
1421

22+
/// Includes the public parameters for in factor state and decrypts the secret material.
1523
fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> {
1624
self.params = Some(serde_json::to_string(&params).unwrap());
1725

@@ -34,6 +42,7 @@ impl FactorDerive for HmacSha1 {
3442
Ok(())
3543
}
3644

45+
/// Computes a new challenge and encrypts the secret material as pad for the factor.
3746
fn params(&self, _key: Key) -> MFKDF2Result<Self::Params> {
3847
let mut challenge = [0u8; 64];
3948
crate::rng::fill_bytes(&mut challenge);
@@ -56,6 +65,75 @@ impl FactorDerive for HmacSha1 {
5665
}
5766
}
5867

68+
/// Factor construction derive phase for an HMAC‑SHA1 factor
69+
///
70+
/// The caller is expected to compute `response = HMAC-SHA1(secret, challenge)` using the secret
71+
/// key material stored by the application and the `challenge` value provided in the setup policy
72+
/// parameters. This helper wraps the response in an [`MFKDF2Factor`] witness Wᵢⱼ that, once
73+
/// combined with the policy via [`FactorDerive::include_params`], recovers the same padded secret
74+
/// as in setup.
75+
///
76+
/// # Errors
77+
///
78+
/// - [MFKDF2Error::MissingDeriveParams](`crate::error::MFKDF2Error::MissingDeriveParams`) if the
79+
/// setup policy omits the `"pad"` parameter when `include_params` is invoked
80+
/// - [MFKDF2Error::InvalidDeriveParams](`crate::error::MFKDF2Error::InvalidDeriveParams`) if the
81+
/// `"pad"` field is not valid hex or has an unexpected shape
82+
///
83+
/// # Example
84+
///
85+
/// Single‑factor setup and factor construction derive phase using the HMAC‑SHA1 factor within
86+
/// KeySetup/KeyDerive:
87+
///
88+
/// ```rust
89+
/// # use std::collections::HashMap;
90+
/// # use mfkdf2::{
91+
/// # error::MFKDF2Result,
92+
/// # setup::{
93+
/// # self,
94+
/// # factors::hmacsha1::{HmacSha1Options, hmacsha1 as setup_hmacsha1},
95+
/// # key::MFKDF2Options,
96+
/// # },
97+
/// # derive,
98+
/// # };
99+
/// # use hmac::{Mac, Hmac};
100+
/// # use sha1::Sha1;
101+
/// # const HMACSHA1_SECRET: [u8; 20] = [0x11; 20];
102+
/// #
103+
/// # fn main() -> MFKDF2Result<()> {
104+
/// // KeySetup: build a policy with a single HMAC‑SHA1 factor
105+
/// let setup_factor = setup_hmacsha1(HmacSha1Options {
106+
/// secret: Some(HMACSHA1_SECRET.to_vec()),
107+
/// ..Default::default()
108+
/// })?;
109+
/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?;
110+
///
111+
/// // Read the challenge for this factor from the policy
112+
/// let policy_factor = setup_key.policy.factors.iter().find(|f| f.id == "hmacsha1").unwrap();
113+
/// let setup_params = &policy_factor.params;
114+
/// let challenge = hex::decode(setup_params["challenge"].as_str().unwrap()).unwrap();
115+
///
116+
/// // The hardware token (or equivalent) computes HMAC-SHA1 over the challenge
117+
/// let response: [u8; 20] = <Hmac<Sha1> as Mac>::new_from_slice(&HMACSHA1_SECRET)
118+
/// .unwrap()
119+
/// .chain_update(&challenge)
120+
/// .finalize()
121+
/// .into_bytes()
122+
/// .into();
123+
///
124+
/// // Build the derive‑time HMAC witness and run KeyDerive
125+
/// let derive_factor = crate::derive::factors::hmacsha1(response.into())?;
126+
/// let derived_key = derive::key(
127+
/// &setup_key.policy,
128+
/// HashMap::from([("hmacsha1".to_string(), derive_factor)]),
129+
/// true,
130+
/// false,
131+
/// )?;
132+
///
133+
/// assert_eq!(derived_key.key, setup_key.key);
134+
/// # Ok(())
135+
/// # }
136+
/// ```
59137
pub fn hmacsha1(response: HmacSha1Response) -> MFKDF2Result<MFKDF2Factor> {
60138
Ok(MFKDF2Factor {
61139
id: None,

mfkdf2/src/derive/factors/hotp.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
//! This module constructs [`MFKDF2Factor`] witnesses Wᵢⱼ for the derive phase corresponding
2+
//! to the setup factors defined in [hotp](`crate::setup::factors::hotp`).
3+
//! - During setup, the HOTP factor chooses a secret target code and encodes an offset and encrypted
4+
//! pad into the policy;
5+
//! - During derive, this module consumes the HOTP code Wᵢⱼ and reconstructs the same target value
6+
//! using the stored offset so that the factor contributes stable material to the key derivation
7+
//! while remaining backward‑compatible with existing OATH HOTP applications.
18
use base64::Engine;
29
use serde_json::{Value, json};
310

@@ -14,6 +21,7 @@ impl FactorDerive for HOTP {
1421
type Output = Value;
1522
type Params = Value;
1623

24+
/// Includes the public parameters for in factor state and calculates the target value.
1725
fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> {
1826
// Store the policy parameters for derive phase
1927
self.params = params.clone();
@@ -28,13 +36,14 @@ impl FactorDerive for HOTP {
2836
let modulus = 10_u64.pow(digits);
2937
let target = (u64::from(offset) + u64::from(self.code)) % modulus;
3038

31-
// Store target as 4-byte big-endian (matches JS implementation)
39+
// Store target as 4-byte big-endian
3240
self.target = target as u32;
3341
}
3442

3543
Ok(())
3644
}
3745

46+
/// Decrypts the secret and generates a new HOTP code with incremented counter.
3847
fn params(&self, key: Key) -> MFKDF2Result<Self::Params> {
3948
let params: HOTPParams = serde_json::from_value(self.params.clone())?;
4049

@@ -61,6 +70,74 @@ impl FactorDerive for HOTP {
6170
}
6271
}
6372

73+
/// HOTP factor construction derive phase
74+
///
75+
/// The code should be the numeric one‑time password displayed by an authenticator app that has
76+
/// been paired with the HOTP secret configured during setup.
77+
///
78+
/// # Errors
79+
///
80+
/// - [`MFKDF2Error::Serialize`](`crate::error::MFKDF2Error::Serialize`) if the stored policy
81+
/// parameters cannot be decoded into [HOTPParams](`crate::setup::factors::hotp::HOTPParams`) (for
82+
/// example, missing or malformed fields)
83+
///
84+
/// # Example
85+
///
86+
/// Single‑factor setup/derive using HOTP within KeySetup/KeyDerive:
87+
///
88+
/// ```rust
89+
/// # use std::collections::HashMap;
90+
/// # use mfkdf2::{
91+
/// # error::MFKDF2Result,
92+
/// # otpauth::HashAlgorithm,
93+
/// # setup::{
94+
/// # self,
95+
/// # factors::hotp::{HOTPOptions, hotp as setup_hotp},
96+
/// # key::MFKDF2Options,
97+
/// # },
98+
/// # derive,
99+
/// # };
100+
/// #
101+
/// # fn main() -> MFKDF2Result<()> {
102+
/// let secret = b"hello world mfkdf2!!".to_vec();
103+
/// let options = HOTPOptions {
104+
/// id: Some("hotp".to_string()),
105+
/// secret: Some(secret),
106+
/// digits: Some(6),
107+
/// hash: Some(HashAlgorithm::Sha1),
108+
/// ..Default::default()
109+
/// };
110+
///
111+
/// let setup_factor = setup_hotp(options)?;
112+
/// let hotp = if let mfkdf2::definitions::FactorType::HOTP(ref h) = setup_factor.factor_type {
113+
/// h.clone()
114+
/// } else {
115+
/// unreachable!()
116+
/// };
117+
/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?;
118+
///
119+
/// let policy_factor = setup_key.policy.factors.iter().find(|f| f.id == "hotp").unwrap();
120+
/// let params = &policy_factor.params;
121+
/// let counter = params["counter"].as_u64().unwrap();
122+
/// let code = mfkdf2::otpauth::generate_otp_token(
123+
/// &hotp.config.secret[..20],
124+
/// counter,
125+
/// &hotp.config.hash,
126+
/// hotp.config.digits,
127+
/// );
128+
///
129+
/// let derive_factor = crate::derive::factors::hotp(code)?;
130+
/// let derived_key = derive::key(
131+
/// &setup_key.policy,
132+
/// HashMap::from([("hotp".to_string(), derive_factor)]),
133+
/// true,
134+
/// false,
135+
/// )?;
136+
///
137+
/// assert_eq!(derived_key.key, setup_key.key);
138+
/// # Ok(())
139+
/// # }
140+
/// ```
64141
pub fn hotp(code: u32) -> MFKDF2Result<MFKDF2Factor> {
65142
// Create HOTP factor with the user-provided code
66143
// The target will be calculated in include_params once we have the policy parameters

mfkdf2/src/derive/factors/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
//! Factor construction derive phase
2+
//!
3+
//! This module constructs [`MFKDF2Factor`] witnesses Wᵢⱼ for the derive phase corresponding
4+
//! to the setup factors defined in [`crate::setup::factors`]. Each helper takes respective factor
5+
//! secret (such as a password, OTP code, UUID, or passkey secret) plus any derive-specific options
6+
//! and constructs a [`MFKDF2Factor`] that is used in [`crate::derive::key`] derivation.
7+
//!
8+
//! During the KeyDerive phase, these factors combine with the public policy state βᵢ to reconstruct
9+
//! the underlying static source material κⱼ and ultimately recover the master secret `M` and next
10+
//! derived key state βᵢ₊₁.
11+
//!
12+
//! **Note:** Factor setup/derive individually are not intended to be used in isolation, but are
13+
//! composed through [`crate::setup::key`] (Setup) and [`crate::derive::key`] (Derive),
14+
//! respectively, where factors supply witness material for the overall multi‑factor policy.
115
mod hmacsha1;
216
mod hotp;
317
mod ooba;

mfkdf2/src/derive/factors/ooba.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
//! This module implements the factor construction derive phase for the OOBA construction from
2+
//! `crate::setup::factors::ooba`.
3+
//! - During setup, the factor samples a random 32‑byte target, encrypts it under a channel‑specific
4+
//! RSA key, and embeds an initial code and metadata in the policy.
5+
//! - During derive, this module consumes a user‑entered OOBA code Wᵢⱼ, decrypts the target using
6+
//! the stored pad, and prepares the next encrypted payload and code for the following login
17
use base64::{Engine, engine::general_purpose};
28
use rsa::Oaep;
39
use serde_json::{Value, json};
@@ -15,6 +21,8 @@ impl FactorDerive for Ooba {
1521
type Output = Value;
1622
type Params = Value;
1723

24+
/// Includes the public parameters for in factor state and decrypts the secret material from
25+
/// public parameters.
1826
fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> {
1927
let pad_b64 =
2028
params["pad"].as_str().ok_or(MFKDF2Error::MissingDeriveParams("pad".to_string()))?;
@@ -42,6 +50,7 @@ impl FactorDerive for Ooba {
4250
Ok(())
4351
}
4452

53+
/// Generates a new OOBA code and encrypts the secret material for the next derivation.
4554
fn params(&self, _key: Key) -> MFKDF2Result<Self::Params> {
4655
let code = generate_alphanumeric_characters(self.length.into()).to_uppercase();
4756

@@ -67,6 +76,87 @@ impl FactorDerive for Ooba {
6776
}
6877
}
6978

79+
/// Factor construction derive phase for an OOBA factor
80+
///
81+
/// The `code` should be the alphanumeric value delivered over the out‑of‑band channel (for example,
82+
/// SMS or push notification) that corresponds to the initial OOBA policy parameters created during
83+
/// setup.
84+
///
85+
/// # Errors
86+
///
87+
/// - [`MFKDF2Error::InvalidOobaCode`] if `code` is empty
88+
/// - [`MFKDF2Error::MissingDeriveParams`] from [`FactorDerive::include_params`] when required
89+
/// fields such as `"pad"` or `"length"` are absent in the policy parameters
90+
/// - [`MFKDF2Error::InvalidDeriveParams`] from [`FactorDerive::include_params`] when fields such as
91+
/// `"pad"`, `"params"`, or `"key"` are malformed or have the wrong type
92+
///
93+
/// # Example
94+
///
95+
/// Single‑factor setup/derive using OOBA within KeySetup/KeyDerive:
96+
///
97+
/// ```rust
98+
/// # use std::collections::HashMap;
99+
/// # use jsonwebtoken::jwk::Jwk;
100+
/// # use rsa::{RsaPrivateKey, RsaPublicKey, traits::PublicKeyParts};
101+
/// # use serde_json::json;
102+
/// # use mfkdf2::{
103+
/// # error::MFKDF2Result,
104+
/// # setup::{
105+
/// # self,
106+
/// # factors::ooba::{ooba as setup_ooba, OobaOptions},
107+
/// # key::MFKDF2Options,
108+
/// # },
109+
/// # derive,
110+
/// # };
111+
/// # use mfkdf2::derive::factors::ooba as derive_ooba;
112+
/// # use base64::Engine;
113+
/// # use sha2::Sha256;
114+
/// # use rsa::Oaep;
115+
/// #
116+
/// # fn main() -> MFKDF2Result<()> {
117+
/// let bits = 2048;
118+
/// let private_key =
119+
/// RsaPrivateKey::new(&mut rsa::rand_core::OsRng, bits).expect("failed to generate a key");
120+
/// let public_key = RsaPublicKey::from(&private_key);
121+
///
122+
/// let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be());
123+
/// let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be());
124+
/// let jwk: Jwk = serde_json::from_value(json!({
125+
/// "kty": "RSA",
126+
/// "alg": "RSA-OAEP-256",
127+
/// "n": n,
128+
/// "e": e
129+
/// }))?;
130+
///
131+
/// let setup_factor = setup_ooba(OobaOptions {
132+
/// id: Some("ooba".into()),
133+
/// length: Some(8),
134+
/// key: Some(jwk),
135+
/// params: Some(json!({"foo": "bar"})),
136+
/// })?;
137+
/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?;
138+
///
139+
/// // Decrypt the first OOBA payload to recover the user-visible code
140+
/// let policy_factor =
141+
/// setup_key.policy.factors.iter().find(|f| f.id == "ooba").unwrap();
142+
/// let setup_params = &policy_factor.params;
143+
/// let ciphertext = hex::decode(setup_params["next"].as_str().unwrap()).unwrap();
144+
/// let plaintext = private_key.decrypt(Oaep::new::<Sha256>(), &ciphertext).unwrap();
145+
/// let decoded: serde_json::Value = serde_json::from_slice(&plaintext).unwrap();
146+
/// let code = decoded["code"].as_str().unwrap();
147+
///
148+
/// let derive_factor = derive_ooba(code)?;
149+
/// let derived_key = derive::key(
150+
/// &setup_key.policy,
151+
/// HashMap::from([("ooba".to_string(), derive_factor)]),
152+
/// true,
153+
/// false,
154+
/// )?;
155+
///
156+
/// assert_eq!(derived_key.key, setup_key.key);
157+
/// # Ok(())
158+
/// # }
159+
/// ```
70160
pub fn ooba(code: &str) -> MFKDF2Result<MFKDF2Factor> {
71161
if code.is_empty() {
72162
return Err(MFKDF2Error::InvalidOobaCode);

0 commit comments

Comments
 (0)