diff --git a/README.md b/README.md index e10f27fe..3950d339 100644 --- a/README.md +++ b/README.md @@ -115,32 +115,44 @@ cargo run --bin uniffi-bindgen generate --library target/debug/libmfkdf2.dylib - ## Usage ```rust -use std::collections::HashMap; -use mfkdf2::{derive, setup}; -use mfkdf2::setup::{factors::hotp::HOTPOptions, password::PasswordOptions, key::MFKDF2Options}; -use mfkdf2::derive::factors::{hotp::HOTPOptions, password::PasswordOptions}; - -fn main() -> Result<(), Box> { - // 1. Define factors - let password_factor = setup::password("my-super-secret-password", PasswordOptions::default())?; - let totp_factor = setup::hotp("base32-encoded-secret", HOTPOptions::default())?; - - // 2. Set up the key with the policy - let key = setup::key(vec![password_factor, totp_factor], MFKDF2Options::default())?; - - println!("Key: {:?}", key); - - let factors = HashMap::from([ - ("password".to_string(), derive::factors::password("my-super-secret-password")?), - "hotp".to_string(), derive::factors::hotp("123456")?), - ]); - // 3. Derive the key using user inputs - let derived_key = derive::key(&key.policy, factors, true, false)?; - - println!("Derived Key: {:?}", derived_key); - - Ok(()) -} +# use std::collections::HashMap; +use mfkdf2::setup::factors::{totp::TOTPOptions, password::PasswordOptions}; +use mfkdf2::definitions::MFKDF2Options; + +let setup = mfkdf2::setup::key( + &[ + mfkdf2::setup::factors::password("password1", PasswordOptions::default())?, + mfkdf2::setup::factors::totp(TOTPOptions { + secret: Some(b"abcdefghijklmnopqrst".to_vec()), + time: Some(1), + ..Default::default() + })?, + ], + MFKDF2Options::default(), +)?; + +let derived_key = mfkdf2::derive::key( + &setup.policy, + HashMap::from([ + ("password".to_string(), mfkdf2::derive::factors::password("password1")?), + ( + "totp".to_string(), + mfkdf2::derive::factors::totp( + 241063, + Some(mfkdf2::derive::factors::totp::TOTPDeriveOptions { + time: Some(30001), + ..Default::default() + }), + )?, + ), + ]), + true, + false, +)?; + +println!("Derived Key: {:?}", derived_key); + +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) ``` ## Development diff --git a/mfkdf2-web/src/api.ts b/mfkdf2-web/src/api.ts index 475bdbd8..08a58748 100644 --- a/mfkdf2-web/src/api.ts +++ b/mfkdf2-web/src/api.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import * as raw from './generated/web/mfkdf2.js'; export { uniffiInitAsync } from './index.web.js'; -export { initRustLogging, LogLevel } from './generated/web/mfkdf2.js'; +export { initLog, LogLevel } from './generated/web/mfkdf2.js'; // Re-export types export type { diff --git a/mfkdf2-web/test/derive/key.test.ts b/mfkdf2-web/test/derive/key.test.ts index 4811d695..8393e342 100644 --- a/mfkdf2-web/test/derive/key.test.ts +++ b/mfkdf2-web/test/derive/key.test.ts @@ -87,7 +87,7 @@ suite('derive/key', () => { .key(setup.policy, { password1: await mfkdf.derive.factors.password('password1') }) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError) + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery) }) }) diff --git a/mfkdf2-web/test/differential/derive.test.ts b/mfkdf2-web/test/differential/derive.test.ts index fda859b5..92a86b61 100644 --- a/mfkdf2-web/test/differential/derive.test.ts +++ b/mfkdf2-web/test/differential/derive.test.ts @@ -5,7 +5,7 @@ chai.use(chaiAsPromised); chai.should(); import { suite, test } from 'mocha'; -import { mfkdf as mfkdf2, uniffiInitAsync, initRustLogging, LogLevel } from '../../src/api'; +import { mfkdf as mfkdf2, uniffiInitAsync } from '../../src/api'; import { derivedKeyIsEqual } from './validation'; import mfkdf from 'mfkdf'; @@ -16,7 +16,6 @@ suite('differential/derive', () => { // Initialize UniFFI once before all tests before(async () => { await uniffiInitAsync(); - await initRustLogging(LogLevel.Debug); }); // each factor individually with single factor diff --git a/mfkdf2-web/test/differential/policy.test.ts b/mfkdf2-web/test/differential/policy.test.ts index e06b18d7..15285d1c 100644 --- a/mfkdf2-web/test/differential/policy.test.ts +++ b/mfkdf2-web/test/differential/policy.test.ts @@ -5,7 +5,7 @@ chai.use(chaiAsPromised); chai.should(); import { suite, test } from 'mocha'; -import mfkdf2, { initRustLogging, LogLevel, uniffiInitAsync } from '../../src/api'; +import mfkdf2, { uniffiInitAsync } from '../../src/api'; import { Mfkdf2Error } from '../../src/generated/web/mfkdf2.js'; import mfkdf from 'mfkdf'; import { derivedKeyIsEqual } from './validation'; @@ -14,7 +14,6 @@ suite('differential/policy', () => { // Initialize UniFFI once before all tests before(async () => { await uniffiInitAsync(); - await initRustLogging(LogLevel.Debug); }); suite('validate', () => { diff --git a/mfkdf2-web/test/differential/reconstitution.test.ts b/mfkdf2-web/test/differential/reconstitution.test.ts index c0fa2a29..d23bcbea 100644 --- a/mfkdf2-web/test/differential/reconstitution.test.ts +++ b/mfkdf2-web/test/differential/reconstitution.test.ts @@ -73,7 +73,7 @@ suite('differential/reconstitution', () => { }, false ) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError); + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery); await setup2.setThreshold(2); @@ -207,7 +207,7 @@ suite('differential/reconstitution', () => { password1: await mfkdf2.derive.factors.password('password1'), password4: await mfkdf2.derive.factors.password('password4') }) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError); + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery); const d5 = await mfkdf.derive.key(setup.policy, { password2: await mfkdf.derive.factors.password('password2'), diff --git a/mfkdf2-web/test/features/reconstitution.test.ts b/mfkdf2-web/test/features/reconstitution.test.ts index c85a87eb..cc156a01 100644 --- a/mfkdf2-web/test/features/reconstitution.test.ts +++ b/mfkdf2-web/test/features/reconstitution.test.ts @@ -35,7 +35,7 @@ suite('features/reconstitution', () => { }, false ) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError); + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery); await setup.setThreshold(2); @@ -82,7 +82,7 @@ suite('features/reconstitution', () => { password1: await mfkdf.derive.factors.password('password1'), password2: await mfkdf.derive.factors.password('password2') }) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError) + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery) await derive2.removeFactor('password2').should.be.rejectedWith(Mfkdf2Error.InvalidThreshold) @@ -99,7 +99,7 @@ suite('features/reconstitution', () => { .key(derive2.policy, { password2: await mfkdf.derive.factors.password('password2') }) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError) + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery) }) test('removeFactors', async () => { @@ -133,7 +133,7 @@ suite('features/reconstitution', () => { password1: await mfkdf.derive.factors.password('password1'), password4: await mfkdf.derive.factors.password('password4') }) - .should.be.rejectedWith(Mfkdf2Error.ShareRecoveryError) + .should.be.rejectedWith(Mfkdf2Error.ShareRecovery) const derive3 = await mfkdf.derive.key(setup.policy, { password2: await mfkdf.derive.factors.password('password2'), diff --git a/mfkdf2/Cargo.toml b/mfkdf2/Cargo.toml index caebbe9d..55195fdf 100644 --- a/mfkdf2/Cargo.toml +++ b/mfkdf2/Cargo.toml @@ -159,3 +159,6 @@ default = [] # Enable UniFFI bindings (FFI exports, scaffolding, bin, etc.) bindings = ["dep:uniffi"] differential-test = [] + +[package.metadata.docs.rs] +rustdoc-args = ["--html-in-header", "katex-header.html"] diff --git a/mfkdf2/README.md b/mfkdf2/README.md new file mode 100644 index 00000000..8d38aa45 --- /dev/null +++ b/mfkdf2/README.md @@ -0,0 +1,480 @@ +# MFKDF2 + +Multi-Factor Key Derivation Function (MFKDF) extends traditional password-based key derivation +by incorporating all of a user’s authentication factors, not just a single secret into the +derivation process. This crate enables constructing high-entropy cryptographic keys from +combinations of passwords, HOTP/TOTP codes, and hardware-backed authenticators such as `YubiKeys`. + +Key capabilities include: +- **Multi-source entropy**: Derive key material from multiple independent factors (passwords, + OTPs, hardware tokens), significantly raising the effective entropy and resistance to offline + brute-force attacks. +- **Factor conjunction**: All required factors must be simultaneously correct to reproduce the + key, creating an exponentially stronger search space than any single factor alone. +- **Threshold recovery**: Optional threshold schemes allow users to recover lost factors without + relying on a central authority, avoiding single points of failure while preserving security + guarantees. +- **Policy-driven authentication**: Keys can encode arbitrarily flexible authentication + policies, enabling cryptographically enforced multi-factor requirements tailored to the + application’s threat model. + +# Factors + +A Factor represents an authentication primitive. Each factor has: +- **Factor material**: the secret input (e.g., a password, TOTP secret, hardware key seed) +- **Public state**: non-secret metadata the factor needs to operate (e.g., counters, + identifiers) + +Examples: +- Password factor stores the password as its material and may use a static ID as its public + state. +- TOTP/HOTP factor stores the shared secret as its material and uses public state such as the + current counter. + +# Supported factors +- Constant entropy factors: + - [`UUID`](`crate::setup::factors::uuid::UUIDFactor`) + - [`Password`](`crate::setup::factors::password::Password`) + - [`Question`](`crate::setup::factors::question::Question`) +- Software Tokens: + - [`HOTP`](`crate::setup::factors::hotp::HOTP`) + - [`TOTP`](`crate::setup::factors::totp::TOTP`) +- Hardware Tokens: + - [`HMACSHA1`](`crate::setup::factors::hmacsha1::HmacSha1`) +- Out-of-band Authentication: + - [`OOBA`](`crate::setup::factors::ooba::Ooba`) +- `WebAuthn` factors: + - [`Passkey`](`crate::setup::factors::passkey::Passkey`) + +Additionally, [`Stack`](`crate::setup::factors::stack::Stack`) and +[`Persisted`](`crate::derive::factors::persisted::Persisted`) factors can be used to modify how a +key is derived. + +# Factor Construction + +A factor's construction defines how the factor is initialized and how it produces key material +over time. It consists of two algorithms: + +## Setup +Initializes the factor with a secret and produces a public state with initial key material. + +```rust +# use mfkdf2::setup::factors::{password, password::PasswordOptions}; +# use mfkdf2::setup::factors::{totp, totp::TOTPOptions}; +# use mfkdf2::error::MFKDF2Error; +# let TOTP_SECRET = vec![0u8; 20]; +# +// setup a password factor with id "pwd" +let password_factor = password("password", PasswordOptions { id: Some("pwd".to_string()) })?; + +// setup a TOTP factor with id "totp" +let totp_factor = totp(TOTPOptions { + id: Some("totp".to_string()), + secret: Some(TOTP_SECRET), + ..Default::default() +})?; +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +## Derive +Takes the factor's witness and produces key material from the factor and the updated + state. + +```rust +# use mfkdf2::derive::factors::{password, totp}; +# use mfkdf2::error::MFKDF2Error; +# +// derive the password factor +let password_factor = password("password")?; + +// derive the TOTP factor with code `123456` +let totp_factor = totp(123456, None)?; +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +# KDF construction + +The MFKDF derivation combines all factor outputs into a single deterministic static key using +`MFKDFSetup` and `MFKDFDerive` algorithms. + +## Setup Key + +Before you can derive a multi-factor derived key, you must setup a "key policy," which specifies +how a key is derived and ensures the key is the same every time (as long as the factors are +correct). + +```rust +# use mfkdf2::setup::factors::{password, password::PasswordOptions}; +# use mfkdf2::setup::factors::{hmacsha1, hmacsha1::HmacSha1Options}; +# use mfkdf2::setup::factors::{hotp, hotp::HOTPOptions}; +# use mfkdf2::setup; +# use mfkdf2::definitions::MFKDF2Options; +# use mfkdf2::error::MFKDF2Error; +# let HOTP_SECRET = vec![0u8; 20]; +# +// perform setup key +let setup_derived_key = setup::key( + &[ + password("password123", PasswordOptions::default()).expect("Failed to setup password factor"), + hmacsha1(HmacSha1Options::default())?, + hotp(HOTPOptions { secret: Some(HOTP_SECRET), ..Default::default() })?, + // add more factors here + ], + MFKDF2Options::default(), +)?; +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +## Derive Key + +After you have setup a key policy, you can derive the key from the policy and the factors. + +1. $\kappa_i \leftarrow \text{Derive}(F_i, \text{witness}_i, \beta_i)$: Per-factor key material +2. $\sigma \leftarrow \text{Combine}(\kappa_1, \kappa_2, \dots, \kappa_n)$: Combine per-factor key material into a single key material +3. $K \leftarrow \text{KDF}(\sigma)$: Final static derived key +4. $\beta_i \leftarrow \text{Update}($F_i, K, \beta_i)$: Optional state update (counters, hardening) + +```text +[F_hmacsha1] --HMAC--> (k_hmacsha1) \ +[F_hotp] --HOTP--> (k_hotp) ---+--> [MFKDF] --> (K) +[F_pw] --PW----> (k_pw) / --> (State B) +``` + +## Examples + +### Password + HOTP + HMACSHA1 + +Derive a composite key with password, hmacsha1 and hotp factors. Derive returns the +[`MFKDF2DerivedKey`](`crate::definitions::MFKDF2DerivedKey`) and updated [`Policy`](`crate::policy::Policy`). + +```rust +# use std::collections::HashMap; +# use mfkdf2::setup::factors::{password as setup_password, password::PasswordOptions}; +# use mfkdf2::setup::factors::{hmacsha1 as setup_hmacsha1, hmacsha1::HmacSha1Options}; +# use mfkdf2::setup::factors::{hotp as setup_hotp, hotp::HOTPOptions}; +# use mfkdf2::setup; +# use mfkdf2::derive::factors::password as derive_password; +# use mfkdf2::derive::factors::hmacsha1 as derive_hmacsha1; +# use mfkdf2::derive::factors::hotp as derive_hotp; +# use mfkdf2::otpauth::generate_otp_token; +# use mfkdf2::derive; +# use mfkdf2::definitions::MFKDF2Options; +# use mfkdf2::error::MFKDF2Error; +# use hmac::{Mac, Hmac}; +# use sha1::Sha1; +# let HOTP_SECRET = vec![0u8; 20]; +# let HMACSHA1_SECRET = vec![0u8; 20]; +# +let setup_password_factor = setup_password("password123", PasswordOptions::default())?; +let setup_hmac_factor = setup_hmacsha1(HmacSha1Options { + secret: Some(HMACSHA1_SECRET.clone()), + ..Default::default() +})?; +let setup_hotp_factor = + setup_hotp(HOTPOptions { secret: Some(HOTP_SECRET.clone()), ..Default::default() })?; + +// perform setup key +let setup_derived_key = setup::key( + &[setup_password_factor, setup_hmac_factor, setup_hotp_factor], + MFKDF2Options::default(), +)?; + +// Derivation phase +let derive_password_factor = derive_password("password123")?; + +# let policy_hmac_factor = setup_derived_key +# .policy +# .factors +# .iter() +# .find(|f| f.id == "hmacsha1").unwrap(); +# let challenge = +# hex::decode(&policy_hmac_factor.params["challenge"].as_str().unwrap()).unwrap(); +# let response: [u8; 20] = as Mac>::new_from_slice(&HMACSHA1_SECRET) +# .unwrap() +# .chain_update(challenge) +# .finalize() +# .into_bytes() +# .into(); +let derive_hmac_factor = derive_hmacsha1(response.into())?; +# +# let policy_hotp_factor = setup_derived_key +# .policy +# .factors +# .iter() +# .find(|f| f.id == "hotp").unwrap(); +# let counter = policy_hotp_factor.params["counter"].as_u64().unwrap(); +# let hash = serde_json::from_value(policy_hotp_factor.params["hash"].clone())?; +# let digits = policy_hotp_factor.params["digits"].as_u64().unwrap() as u32; +# let correct_code = generate_otp_token(&HOTP_SECRET, counter, &hash, digits); +let derive_hotp_factor = derive_hotp(correct_code as u32)?; + +let derived_key = derive::key( + &setup_derived_key.policy, + HashMap::from([ + (String::from("password"), derive_password_factor), + (String::from("hmacsha1"), derive_hmac_factor), + (String::from("hotp"), derive_hotp_factor), + ]), + false, + false, +)?; + +// derived_key.key -> 34d2…5771 +# assert_eq!(derived_key.key, setup_derived_key.key); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +### Password + TOTP + +```rust +# use std::collections::HashMap; +use mfkdf2::setup::factors::{totp::TOTPOptions, password::PasswordOptions}; +use mfkdf2::definitions::MFKDF2Options; + +let setup = mfkdf2::setup::key( + &[ + mfkdf2::setup::factors::password("password1", PasswordOptions::default())?, + mfkdf2::setup::factors::totp(TOTPOptions { + secret: Some(b"abcdefghijklmnopqrst".to_vec()), + time: Some(1), + ..Default::default() + })?, + ], + MFKDF2Options::default(), +)?; + +let derived_key = mfkdf2::derive::key( + &setup.policy, + HashMap::from([ + ("password".to_string(), mfkdf2::derive::factors::password("password1")?), + ( + "totp".to_string(), + mfkdf2::derive::factors::totp( + 241063, + Some(mfkdf2::derive::factors::totp::TOTPDeriveOptions { + time: Some(30001), + ..Default::default() + }), + )?, + ), + ]), + true, + false, +)?; + +println!("Derived Key: {:?}", derived_key); + +# assert_eq!(setup.key, derived_key.key); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +# Threshold Recovery + +Threshold recovery generalizes a multi‑factor policy from “all factors required” to a +configurable `t`‑of‑`n` requirement. During setup, the derived secret is split into shares using +a Shamir‑style secret sharing scheme, one share per factor. During derive, any subset of factors +that supplies at least `threshold` valid shares can reconstruct the same secret and therefore +the same derived key. + +## Setup: configuring a 2‑of‑3 recovery policy + +The snippet below constructs a 2‑of‑3 key from a password, an HOTP soft token, and a UUID +recovery code. Any 2 of these 3 factors are sufficient to reproduce the key. + +```rust +# use std::collections::HashMap; +# use mfkdf2::error::MFKDF2Error; +# use mfkdf2::{setup, derive}; +# use mfkdf2::setup::factors::{password::PasswordOptions, hotp::HOTPOptions}; +# use mfkdf2::setup::factors::{uuid::UUIDOptions}; +# use mfkdf2::derive::factors::hotp as derive_hotp; +# use mfkdf2::derive::factors::uuid as derive_uuid; +# use mfkdf2::otpauth::{generate_otp_token, HashAlgorithm}; +# use mfkdf2::definitions::MFKDF2Options; +# use uuid::Uuid; +# +// setup phase: construct factors +let password_factor = setup::factors::password("password123", PasswordOptions::default())?; + +// HOTP uses a random secret and 6‑digit codes by default +let setup_hotp_factor = setup::factors::hotp(HOTPOptions::default())?; +let hotp_state = match &setup_hotp_factor.factor_type { + mfkdf2::definitions::FactorType::HOTP(h) => h.clone(), + _ => unreachable!("HOTPOptions always produce an HOTP factor"), +}; + +// UUID factor uses a stable UUID as a recovery code +let setup_uuid_factor = + setup::factors::uuid(UUIDOptions { uuid: Some(Uuid::nil()), ..UUIDOptions::default() })?; + +// configure a 2‑of‑3 threshold policy +let options = MFKDF2Options { threshold: Some(2), ..MFKDF2Options::default() }; +let setup_derived = setup::key(&[password_factor, setup_hotp_factor, setup_uuid_factor], options)?; + +// derive phase: build inputs for any 2 factors +let policy_hotp_factor: &mfkdf2::policy::PolicyFactor = setup_derived + .policy + .factors + .iter() + .find(|f| f.id == "hotp") + .expect("policy must contain an HOTP factor"); + +let counter = policy_hotp_factor.params["counter"].as_u64().expect("counter must be present"); +let hash: HashAlgorithm = + serde_json::from_value(policy_hotp_factor.params["hash"].clone()).expect("hash must decode"); +let digits = policy_hotp_factor.params["digits"].as_u64().expect("digits must be present") as u32; +let hotp_secret = &hotp_state.config.secret[..20]; +let correct_code = generate_otp_token(hotp_secret, counter, &hash, digits); + +let mut derive_factors = HashMap::new(); + +// HOTP factor provided by the current OTP displayed in the authenticator app +let mut derive_hotp_factor = derive_hotp(correct_code as u32)?; +derive_hotp_factor.id = Some("hotp".to_string()); +derive_factors.insert("hotp".to_string(), derive_hotp_factor); + +// UUID factor provided by the user's stored recovery code +let mut derive_uuid_factor = derive_uuid(Uuid::nil())?; +derive_uuid_factor.id = Some("uuid".to_string()); +derive_factors.insert("uuid".to_string(), derive_uuid_factor); + +// only 2 out of the 3 factors are provided here +let derived = derive::key(&setup_derived.policy, derive_factors, true, false)?; +assert_eq!(derived.key, setup_derived.key); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +Threshold value must be between 1 and the number of factors, otherwise +[`MFKDF2Error::InvalidThreshold`](`crate::error::MFKDF2Error::InvalidThreshold`) is returned. + +```rust +# use mfkdf2::error::{MFKDF2Error, MFKDF2Result}; +# use mfkdf2::setup::factors::{password, password::PasswordOptions}; +# use mfkdf2::setup; +# use mfkdf2::definitions::MFKDF2Options; +# +// requesting 2‑of‑1 factors causes MFKDF2Error::InvalidThreshold +let result = + setup::key(&[password("password123", PasswordOptions::default())?], MFKDF2Options { threshold: Some(2), ..MFKDF2Options::default() }); +assert!(matches!(result, Err(MFKDF2Error::InvalidThreshold))); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +If insufficient factors are provided, share recovery fails and +[`MFKDF2Error::ShareRecovery`](`crate::error::MFKDF2Error::ShareRecovery`) is returned. + +```rust +# use std::collections::HashMap; +# +# use mfkdf2::error::{MFKDF2Error, MFKDF2Result}; +# use mfkdf2::setup::factors::{password as setup_password, password::PasswordOptions}; +# use mfkdf2::setup; +# use mfkdf2::definitions::MFKDF2Options; +# use mfkdf2::derive::factors::password as derive_password; +# use mfkdf2::derive; +# +// setup phase with a 2‑of‑2 password policy +let setup_factors = &[ + setup_password("primary‑password", PasswordOptions { id: Some("pw1".into()) })?, + setup_password("backup‑password", PasswordOptions { id: Some("pw2".into()) })?, +]; +let options = MFKDF2Options { threshold: Some(2), ..MFKDF2Options::default() }; +let setup_derived = setup::key(setup_factors, options)?; + +// derive phase provides only one out of the two required factors +let mut derive_factors = HashMap::new(); +let mut derive_pw1 = derive_password("primary‑password")?; +derive_pw1.id = Some("pw1".into()); +derive_factors.insert("pw1".into(), derive_pw1); + +let result = derive::key(&setup_derived.policy, derive_factors, true, false); +assert!(matches!(result, Err(MFKDF2Error::ShareRecovery))); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +These examples illustrate how a `t`‑of‑`n` MFKDF2 policy can express flexible recovery flows +(such as 2‑of‑3 password + HOTP + UUID or 3‑of‑5 enterprise policies) while preserving +cryptographic guarantees about the minimum factor set required to unlock a key. + +# Key Stacking + +Key stacking treats a derived key from one MFKDF2 policy as a reusable factor in another policy. +A stack factor wraps a complete inner policy and derived key, enabling nested constructions such +as `(password₁ ∧ password₂) ∨ password₃` or more elaborate hierarchies. + +Direct use of [`Stack`](`crate::setup::factors::stack::stack`) Factor mainly serves advanced use cases; most +applications prefer configuring policies through higher‑level factor combinations and +thresholds. + +## Example: `(password₁ ∧ password₂) ∨ password₃` + +The following example configures a 2‑of‑2 inner stack over two passwords and an outer 1‑of‑2 +policy between the stack and a third password: + +```rust +# use std::collections::HashMap; +# +# use mfkdf2::error::MFKDF2Error; +# use mfkdf2::setup::factors::{password as setup_password, password::PasswordOptions}; +# use mfkdf2::setup::factors::{stack as setup_stack, stack::StackOptions}; +# use mfkdf2::setup; +# use mfkdf2::derive::factors::password as derive_password; +# use mfkdf2::derive::factors::stack as derive_stack; +# use mfkdf2::derive; +# use mfkdf2::definitions::MFKDF2Options; +# +// inner stack: password₁ ∧ password₂ +let inner = vec![ + setup_password("password1", PasswordOptions { id: Some("password1".into()) })?, + setup_password("password2", PasswordOptions { id: Some("password2".into()) })?, +]; + +let stacked = setup_stack(inner, StackOptions { + id: Some("stack".into()), + threshold: Some(2), + salt: None, +})?; + +// outer policy: (password₁ ∧ password₂) ∨ password₃ +let password3 = setup_password("password3", PasswordOptions { id: Some("password3".into()) })?; + +let setup_derived = setup::key(&[stacked, password3], MFKDF2Options { + threshold: Some(1), + ..MFKDF2Options::default() +})?; + +// derive with password₁ and password₂ through a stack factor +let derive_stack_factor = derive_stack(HashMap::from([ + ("password1".to_string(), derive_password("password1")?), + ("password2".to_string(), derive_password("password2")?), +]))?; + +let derived = derive::key( + &setup_derived.policy, + HashMap::from([("stack".to_string(), derive_stack_factor)]), + false, + false, +)?; + +# assert_eq!(derived.key, setup_derived.key); +# Ok::<(), mfkdf2::error::MFKDF2Error>(()) +``` + +The same outer key can also be derived with only `password3` by supplying a single password +factor keyed by `"password3"` to [setup key](`crate::derive::key`). + +# Feature Flags + +- `bindings`: Generate FFI bindings of the library to other languages. +- `differential-test`: Enable changes required for deterministic testing. + +# Differential Testing + +Differential testing is used to ensure the correctness of the library. It is enabled by the +`differential-test` feature flag. It is performed by comparing the output of the library with +the output of the reference implementation. + +The reference implementation is the JavaScript implementation of the MFKDF2 spec. It is available at +[MFKDF](https://github.com/multifactor/mfkdf). + + diff --git a/mfkdf2/benches/factor_combination.rs b/mfkdf2/benches/factor_combination.rs index 5005823d..4ab56678 100644 --- a/mfkdf2/benches/factor_combination.rs +++ b/mfkdf2/benches/factor_combination.rs @@ -2,9 +2,10 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, derive::factors::totp::TOTPDeriveOptions, - otpauth::{HashAlgorithm, generate_hotp_code}, + otpauth::{HashAlgorithm, generate_otp_token}, setup::{ self, factors::{ @@ -14,7 +15,6 @@ use mfkdf2::{ totp::{TOTPOptions, totp}, uuid::{UUIDOptions, uuid}, }, - key::MFKDF2Options, }, }; use uuid::Uuid; @@ -52,7 +52,7 @@ fn bench_factor_combination_setup(c: &mut Criterion) { }) .unwrap(), ]); - let result = black_box(setup::key::key(&factors, MFKDF2Options::default())); + let result = black_box(setup::key(&factors, MFKDF2Options::default())); result.unwrap() }) }); @@ -76,7 +76,7 @@ fn bench_factor_combination_setup(c: &mut Criterion) { }) .unwrap(), ]); - let result = black_box(setup::key::key(&factors, MFKDF2Options::default())); + let result = black_box(setup::key(&factors, MFKDF2Options::default())); result.unwrap() }) }); @@ -101,18 +101,18 @@ fn bench_factor_combination_derive(c: &mut Criterion) { .unwrap(); let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; - let setup_key = setup::key::key(&[factor1, factor2, factor3], options).unwrap(); + let setup_key = setup::key(&[factor1, factor2, factor3], options).unwrap(); // Pre-compute HOTP code for derive let policy_hotp_factor = setup_key.policy.factors.iter().find(|f| f.id == "hotp").unwrap(); let hotp_params = &policy_hotp_factor.params; let counter = hotp_params["counter"].as_u64().unwrap(); - let hotp_code = generate_hotp_code(&SECRET20, counter, &HashAlgorithm::Sha1, 6); + let hotp_code = generate_otp_token(&SECRET20, counter, &HashAlgorithm::Sha1, 6); // Pre-compute TOTP code for derive let time = 1; let totp_counter = time / 30; - let totp_code = generate_hotp_code(&SECRET20, totp_counter, &HashAlgorithm::Sha1, 6); + let totp_code = generate_otp_token(&SECRET20, totp_counter, &HashAlgorithm::Sha1, 6); // Benchmark derive with password + hotp (threshold 2 of 3) group.bench_function("derive_password_hotp", |b| { diff --git a/mfkdf2/benches/hmacsha1.rs b/mfkdf2/benches/hmacsha1.rs index db90dc94..a6152095 100644 --- a/mfkdf2/benches/hmacsha1.rs +++ b/mfkdf2/benches/hmacsha1.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::hmacsha1::{HmacSha1Options, HmacSha1Response, hmacsha1}, - key::MFKDF2Options, }, }; @@ -24,13 +24,13 @@ fn bench_hmacsha1(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 HMACSHA1 - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[hmacsha1(HmacSha1Options { id: Some("hmac".to_string()), secret: Some(SECRET20.to_vec()), @@ -74,13 +74,13 @@ fn bench_hmacsha1(c: &mut Criterion) { .unwrap(), ]); let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key(&factors, options)); + let result = black_box(setup::key(&factors, options)); result.unwrap() }) }); // Multiple derive - 3 HMACSHA1 (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ hmacsha1(HmacSha1Options { id: Some("hmac1".to_string()), @@ -129,7 +129,7 @@ fn bench_hmacsha1(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 HMACSHA1 - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ hmacsha1(HmacSha1Options { id: Some("hmac1".to_string()), diff --git a/mfkdf2/benches/hotp.rs b/mfkdf2/benches/hotp.rs index 68c2a9d0..781d0895 100644 --- a/mfkdf2/benches/hotp.rs +++ b/mfkdf2/benches/hotp.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::hotp::{HOTPOptions, hotp as setup_hotp}, - key::MFKDF2Options, }, }; @@ -25,13 +25,13 @@ fn bench_hotp(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 HOTP - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_hotp(HOTPOptions { id: Some("hotp".to_string()), secret: Some(SECRET20.to_vec()), @@ -77,13 +77,13 @@ fn bench_hotp(c: &mut Criterion) { .unwrap(), ]); let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key(&factors, options)); + let result = black_box(setup::key(&factors, options)); result.unwrap() }) }); // Multiple derive - 3 HOTPs (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_hotp(HOTPOptions { id: Some("hotp1".to_string()), @@ -123,7 +123,7 @@ fn bench_hotp(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 HOTPs - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_hotp(HOTPOptions { id: Some("hotp1".to_string()), diff --git a/mfkdf2/benches/mfdpg.rs b/mfkdf2/benches/mfdpg.rs index 3fe25acc..873bb3f9 100644 --- a/mfkdf2/benches/mfdpg.rs +++ b/mfkdf2/benches/mfdpg.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::password::{PasswordOptions, password as setup_password}, - key::MFKDF2Options, }, }; @@ -14,7 +14,7 @@ fn bench_mfdpg(c: &mut Criterion) { let mut group = c.benchmark_group("mfdpg"); // Setup a derived key for password derivation benchmarks - let setup_key = setup::key::key( + let setup_key = setup::key( &[setup_password("password1", PasswordOptions { id: Some("password".to_string()) }).unwrap()], MFKDF2Options::default(), ) @@ -31,39 +31,51 @@ fn bench_mfdpg(c: &mut Criterion) { // Simple regex pattern: alphanumeric, fixed length group.bench_function("derive_password_simple", |b| { b.iter(|| { - black_box(derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}")); + black_box( + derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}").unwrap(), + ); }) }); // Medium complexity: alphabetic, variable length group.bench_function("derive_password_medium", |b| { b.iter(|| { - black_box(derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")); + black_box( + derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}").unwrap(), + ); }) }); // Complex regex pattern: mixed alphanumeric with specific structure group.bench_function("derive_password_complex", |b| { b.iter(|| { - black_box(derived_key.derive_password( - Some("example.com"), - Some(b"salt"), - "([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]*", - )); + black_box( + derived_key + .derive_password( + Some("example.com"), + Some(b"salt"), + "([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]*", + ) + .unwrap(), + ); }) }); // Very simple pattern: just digits group.bench_function("derive_password_digits_only", |b| { b.iter(|| { - black_box(derived_key.derive_password(Some("example.com"), Some(b"salt"), "[0-9]{6}")); + black_box( + derived_key.derive_password(Some("example.com"), Some(b"salt"), "[0-9]{6}").unwrap(), + ); }) }); // Long pattern: longer password group.bench_function("derive_password_long", |b| { b.iter(|| { - black_box(derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{16}")); + black_box( + derived_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{16}").unwrap(), + ); }) }); } diff --git a/mfkdf2/benches/ooba.rs b/mfkdf2/benches/ooba.rs index 83c0cdf3..fa380fb2 100644 --- a/mfkdf2/benches/ooba.rs +++ b/mfkdf2/benches/ooba.rs @@ -3,12 +3,12 @@ use std::{collections::HashMap, hint::black_box}; use base64::Engine; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, policy::Policy, setup::{ self, factors::ooba::{OobaOptions, ooba as setup_ooba}, - key::MFKDF2Options, }, }; use rsa::{Oaep, RsaPrivateKey, traits::PublicKeyParts}; @@ -60,14 +60,14 @@ fn bench_ooba(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 OOBA let (jwk, private_key) = create_keypair(2048); - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_ooba(OobaOptions { id: Some("ooba".to_string()), key: Some(serde_json::from_value(jwk).unwrap()), @@ -123,7 +123,7 @@ fn bench_ooba(c: &mut Criterion) { .unwrap(), ]); let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key(&factors, options)); + let result = black_box(setup::key(&factors, options)); result.unwrap() }) }); @@ -133,7 +133,7 @@ fn bench_ooba(c: &mut Criterion) { let (jwk2, private_key2) = create_keypair(2048); let (jwk3, private_key3) = create_keypair(2048); - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_ooba(OobaOptions { id: Some("ooba1".to_string()), @@ -185,7 +185,7 @@ fn bench_ooba(c: &mut Criterion) { let (jwk2, private_key2) = create_keypair(2048); let (jwk3, _private_key3) = create_keypair(2048); - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_ooba(OobaOptions { id: Some("ooba1".to_string()), diff --git a/mfkdf2/benches/passkey.rs b/mfkdf2/benches/passkey.rs index 2def8bdd..2504c68c 100644 --- a/mfkdf2/benches/passkey.rs +++ b/mfkdf2/benches/passkey.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::passkey::{PasskeyOptions, passkey as setup_passkey}, - key::MFKDF2Options, }, }; @@ -17,14 +17,14 @@ fn bench_setup_passkey(c: &mut Criterion) { b.iter(|| { let secret = [42u8; 32]; let factor = black_box(setup_passkey(secret, PasskeyOptions::default()).unwrap()); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 passkey let secret = [42u8; 32]; - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_passkey(secret, PasskeyOptions { id: Some("passkey".to_string()) }).unwrap()], MFKDF2Options::default(), ) @@ -45,7 +45,7 @@ fn bench_setup_passkey(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ setup_passkey([1u8; 32], PasskeyOptions { id: Some("passkey1".to_string()) }).unwrap(), setup_passkey([2u8; 32], PasskeyOptions { id: Some("passkey2".to_string()) }).unwrap(), @@ -58,7 +58,7 @@ fn bench_setup_passkey(c: &mut Criterion) { }); // Multiple derive - 3 passkeys (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_passkey([1u8; 32], PasskeyOptions { id: Some("passkey1".to_string()) }).unwrap(), setup_passkey([2u8; 32], PasskeyOptions { id: Some("passkey2".to_string()) }).unwrap(), @@ -81,7 +81,7 @@ fn bench_setup_passkey(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 passkeys - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_passkey([1u8; 32], PasskeyOptions { id: Some("passkey1".to_string()) }).unwrap(), setup_passkey([2u8; 32], PasskeyOptions { id: Some("passkey2".to_string()) }).unwrap(), diff --git a/mfkdf2/benches/password.rs b/mfkdf2/benches/password.rs index ee90e79d..1fefe092 100644 --- a/mfkdf2/benches/password.rs +++ b/mfkdf2/benches/password.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::password::{PasswordOptions, password as setup_password}, - key::MFKDF2Options, }, }; @@ -16,13 +16,13 @@ fn bench_password(c: &mut Criterion) { group.bench_function("single_setup", |b| { b.iter(|| { let factor = black_box(setup_password("password1", PasswordOptions::default()).unwrap()); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 password - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_password("password1", PasswordOptions { id: Some("pwd".to_string()) }).unwrap()], MFKDF2Options::default(), ) @@ -43,7 +43,7 @@ fn bench_password(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ setup_password("password1", PasswordOptions { id: Some("pwd1".to_string()) }).unwrap(), setup_password("password2", PasswordOptions { id: Some("pwd2".to_string()) }).unwrap(), @@ -56,7 +56,7 @@ fn bench_password(c: &mut Criterion) { }); // Multiple derive - 3 passwords (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_password("password1", PasswordOptions { id: Some("pwd1".to_string()) }).unwrap(), setup_password("password2", PasswordOptions { id: Some("pwd2".to_string()) }).unwrap(), @@ -79,7 +79,7 @@ fn bench_password(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 passwords - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_password("password1", PasswordOptions { id: Some("pwd1".to_string()) }).unwrap(), setup_password("password2", PasswordOptions { id: Some("pwd2".to_string()) }).unwrap(), diff --git a/mfkdf2/benches/question.rs b/mfkdf2/benches/question.rs index 18b58a64..738bb95f 100644 --- a/mfkdf2/benches/question.rs +++ b/mfkdf2/benches/question.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::question::{QuestionOptions, question as setup_question}, - key::MFKDF2Options, }, }; @@ -22,13 +22,13 @@ fn bench_question(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 question - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_question("answer1", QuestionOptions { id: Some("question".to_string()), question: Some("What is your favorite color?".to_string()), @@ -53,7 +53,7 @@ fn bench_question(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ setup_question("answer1", QuestionOptions { id: Some("q1".to_string()), @@ -78,7 +78,7 @@ fn bench_question(c: &mut Criterion) { }); // Multiple derive - 3 questions (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_question("answer1", QuestionOptions { id: Some("q1".to_string()), @@ -113,7 +113,7 @@ fn bench_question(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 questions - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_question("answer1", QuestionOptions { id: Some("q1".to_string()), diff --git a/mfkdf2/benches/reconstitution.rs b/mfkdf2/benches/reconstitution.rs index efc1451f..52cab0f2 100644 --- a/mfkdf2/benches/reconstitution.rs +++ b/mfkdf2/benches/reconstitution.rs @@ -1,10 +1,12 @@ use std::hint::black_box; use criterion::{Criterion, criterion_group, criterion_main}; -use mfkdf2::setup::{ - self, - factors::password::{PasswordOptions, password as setup_password}, - key::MFKDF2Options, +use mfkdf2::{ + definitions::MFKDF2Options, + setup::{ + self, + factors::password::{PasswordOptions, password as setup_password}, + }, }; fn bench_reconstitution(c: &mut Criterion) { diff --git a/mfkdf2/benches/stack.rs b/mfkdf2/benches/stack.rs index 0361fa15..0093882e 100644 --- a/mfkdf2/benches/stack.rs +++ b/mfkdf2/benches/stack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ - definitions::MFKDF2Factor, + definitions::{MFKDF2Factor, MFKDF2Options}, derive, setup::{ self, @@ -10,7 +10,6 @@ use mfkdf2::{ password::{PasswordOptions, password as setup_password}, stack::{StackOptions, stack as setup_stack}, }, - key::MFKDF2Options, }, }; @@ -57,13 +56,13 @@ fn bench_setup_stack(c: &mut Criterion) { group.bench_function("single_setup", |b| { b.iter(|| { let factor = black_box(create_stack_factor("stack", "p1", "pw1", "p2", "pw2").unwrap()); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 stack - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[create_stack_factor("stack", "p1", "pw1", "p2", "pw2").unwrap()], MFKDF2Options::default(), ) @@ -81,7 +80,7 @@ fn bench_setup_stack(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ create_stack_factor("s1", "s1p1", "s1p1", "s1p2", "s1p2").unwrap(), create_stack_factor("s2", "s2p1", "s2p1", "s2p2", "s2p2").unwrap(), @@ -94,7 +93,7 @@ fn bench_setup_stack(c: &mut Criterion) { }); // Multiple derive - 3 stacks (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ create_stack_factor("s1", "s1p1", "s1p1", "s1p2", "s1p2").unwrap(), create_stack_factor("s2", "s2p1", "s2p1", "s2p2", "s2p2").unwrap(), @@ -138,7 +137,7 @@ fn bench_setup_stack(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 stacks - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ create_stack_factor("s1", "s1p1", "s1p1", "s1p2", "s1p2").unwrap(), create_stack_factor("s2", "s2p1", "s2p1", "s2p2", "s2p2").unwrap(), diff --git a/mfkdf2/benches/totp.rs b/mfkdf2/benches/totp.rs index 569d4275..95738ddd 100644 --- a/mfkdf2/benches/totp.rs +++ b/mfkdf2/benches/totp.rs @@ -6,13 +6,13 @@ use std::{ use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, derive::factors::totp::TOTPDeriveOptions, - otpauth::{HashAlgorithm, generate_hotp_code}, + otpauth::{HashAlgorithm, generate_otp_token}, setup::{ self, factors::totp::{TOTPOptions, totp as setup_totp}, - key::MFKDF2Options, }, }; @@ -39,13 +39,13 @@ fn bench_totp(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 TOTP - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_totp(TOTPOptions { id: Some("totp".to_string()), secret: Some(SECRET20.to_vec()), @@ -65,7 +65,7 @@ fn bench_totp(c: &mut Criterion) { b.iter(|| { // Create an oracle that provides the correct TOTP code for any time let mut oracle = HashMap::new(); - let totp_code = generate_hotp_code(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6); + let totp_code = generate_otp_token(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6); oracle.insert(current_time / 30, totp_code); let factors_map = black_box(HashMap::from([( @@ -87,7 +87,7 @@ fn bench_totp(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ setup_totp(TOTPOptions { id: Some("totp1".to_string()), @@ -132,7 +132,7 @@ fn bench_totp(c: &mut Criterion) { // Multiple derive - 3 TOTPs (all required) current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_totp(TOTPOptions { id: Some("totp1".to_string()), @@ -178,7 +178,7 @@ fn bench_totp(c: &mut Criterion) { ( "totp1".to_string(), derive::factors::totp( - generate_hotp_code(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6), + generate_otp_token(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6), None, ) .unwrap(), @@ -186,7 +186,7 @@ fn bench_totp(c: &mut Criterion) { ( "totp2".to_string(), derive::factors::totp( - generate_hotp_code( + generate_otp_token( &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], current_time / 30, &HashAlgorithm::Sha1, @@ -199,7 +199,7 @@ fn bench_totp(c: &mut Criterion) { ( "totp3".to_string(), derive::factors::totp( - generate_hotp_code( + generate_otp_token( &[21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], current_time / 30, &HashAlgorithm::Sha1, @@ -217,7 +217,7 @@ fn bench_totp(c: &mut Criterion) { // Threshold derive - 2 out of 3 TOTPs current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_totp(TOTPOptions { id: Some("totp1".to_string()), @@ -263,7 +263,7 @@ fn bench_totp(c: &mut Criterion) { ( "totp1".to_string(), derive::factors::totp( - generate_hotp_code(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6), + generate_otp_token(&SECRET20, current_time / 30, &HashAlgorithm::Sha1, 6), None, ) .unwrap(), @@ -271,7 +271,7 @@ fn bench_totp(c: &mut Criterion) { ( "totp2".to_string(), derive::factors::totp( - generate_hotp_code( + generate_otp_token( &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], current_time / 30, &HashAlgorithm::Sha1, diff --git a/mfkdf2/benches/uuid.rs b/mfkdf2/benches/uuid.rs index f7dc2b33..1cbc2da6 100644 --- a/mfkdf2/benches/uuid.rs +++ b/mfkdf2/benches/uuid.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, hint::black_box}; use criterion::{Criterion, criterion_group, criterion_main}; use mfkdf2::{ + definitions::MFKDF2Options, derive, setup::{ self, factors::uuid::{UUIDOptions, uuid as setup_uuid}, - key::MFKDF2Options, }, }; use uuid::Uuid; @@ -23,13 +23,13 @@ fn bench_uuid(c: &mut Criterion) { }) .unwrap(), ); - let result = black_box(setup::key::key(&[factor], MFKDF2Options::default())); + let result = black_box(setup::key(&[factor], MFKDF2Options::default())); result.unwrap() }) }); // Single derive - 1 UUID - let single_setup_key = setup::key::key( + let single_setup_key = setup::key( &[setup_uuid(UUIDOptions { id: Some("uuid".to_string()), uuid: Some(Uuid::parse_str("9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d").unwrap()), @@ -55,7 +55,7 @@ fn bench_uuid(c: &mut Criterion) { group.bench_function("multiple_setup_3_threshold_3", |b| { b.iter(|| { let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; - let result = black_box(setup::key::key( + let result = black_box(setup::key( &[ setup_uuid(UUIDOptions { id: Some("uuid1".to_string()), @@ -80,7 +80,7 @@ fn bench_uuid(c: &mut Criterion) { }); // Multiple derive - 3 UUIDs (all required) - let multiple_setup_key_3 = setup::key::key( + let multiple_setup_key_3 = setup::key( &[ setup_uuid(UUIDOptions { id: Some("uuid1".to_string()), @@ -127,7 +127,7 @@ fn bench_uuid(c: &mut Criterion) { }); // Threshold derive - 2 out of 3 UUIDs - let threshold_setup_key = setup::key::key( + let threshold_setup_key = setup::key( &[ setup_uuid(UUIDOptions { id: Some("uuid1".to_string()), diff --git a/mfkdf2/katex-header.html b/mfkdf2/katex-header.html new file mode 100644 index 00000000..8b2e1c3b --- /dev/null +++ b/mfkdf2/katex-header.html @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/mfkdf2/src/constants.rs b/mfkdf2/src/constants.rs index 7eb9dc67..ac5d65a1 100644 --- a/mfkdf2/src/constants.rs +++ b/mfkdf2/src/constants.rs @@ -1,2 +1,4 @@ +//! Constants for MFKDF2. + /// Irreducible polynomial used in the secret sharing scheme: x⁸ + x⁴ + x³ + x + 1 pub const SECRET_SHARING_POLY: u16 = 0x11B; diff --git a/mfkdf2/src/crypto.rs b/mfkdf2/src/crypto.rs index beaf2660..df31bfd2 100644 --- a/mfkdf2/src/crypto.rs +++ b/mfkdf2/src/crypto.rs @@ -1,3 +1,4 @@ +//! Cryptographic functions for the MFKDF2 library. use aes::Aes256; use cipher::{BlockDecryptMut, BlockEncryptMut, KeyInit, block_padding::NoPadding}; use ecb::{Decryptor, Encryptor}; @@ -6,15 +7,16 @@ use hmac::{Hmac, Mac}; use sha1::Sha1; use sha2::Sha256; -pub fn hkdf_sha256_with_info(input: &[u8], salt: &[u8], info: &[u8]) -> [u8; 32] { +/// Derives a 32-byte key using HKDF-SHA256 with the given salt and info. +pub(crate) fn hkdf_sha256_with_info(input: &[u8], salt: &[u8], info: &[u8]) -> [u8; 32] { let hk = Hkdf::::new(Some(salt), input); let mut okm = [0u8; 32]; hk.expand(info, &mut okm).expect("HKDF expand"); okm } -/// Encrypts a buffer using AES256-ECB with the given 32-byte key -pub fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec { +/// Encrypts a buffer using AES256-ECB with the given 32-byte key. +pub(crate) fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec { // Ensure the input is a multiple of 16 by zero-padding if necessary. let mut buf = { let mut v = data.to_vec(); @@ -31,21 +33,23 @@ pub fn encrypt(data: &[u8], key: &[u8; 32]) -> Vec { buf } -/// Decrypts a buffer using AES256-ECB with the given 32-byte key +/// 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 fn decrypt(mut data: Vec, key: &[u8; 32]) -> Vec { +pub(crate) fn decrypt(mut data: Vec, key: &[u8; 32]) -> Vec { let cipher = Decryptor::::new_from_slice(key).expect("Invalid AES key"); let _ = cipher.decrypt_padded_mut::(&mut data).expect("ECB decrypt"); data } -pub fn hmacsha1(secret: &[u8], challenge: &[u8]) -> [u8; 20] { +/// Computes an HMAC-SHA1 over the given challenge using the provided secret. +pub(crate) fn hmacsha1(secret: &[u8], challenge: &[u8]) -> [u8; 20] { let mut mac = as Mac>::new_from_slice(secret).unwrap(); mac.update(challenge); mac.finalize().into_bytes().into() } -pub fn hmacsha256(secret: &[u8], input: &[u8]) -> [u8; 32] { +/// Computes an HMAC-SHA256 over the given input using the provided secret. +pub(crate) fn hmacsha256(secret: &[u8], input: &[u8]) -> [u8; 32] { let mut mac = as Mac>::new_from_slice(secret).unwrap(); mac.update(input); mac.finalize().into_bytes().into() diff --git a/mfkdf2/src/definitions/bytearray.rs b/mfkdf2/src/definitions/bytearray.rs index 579dbcbb..f8a4d898 100644 --- a/mfkdf2/src/definitions/bytearray.rs +++ b/mfkdf2/src/definitions/bytearray.rs @@ -1,3 +1,5 @@ +//! Generic `ByteArray` type for FFI boundaries, typically used for fixed-size byte arrays like keys +//! and salts. use serde::{Deserialize, Serialize}; /// Generic fixed-size byte array used as the basis for key-like types. diff --git a/mfkdf2/src/definitions/entropy.rs b/mfkdf2/src/definitions/entropy.rs index 1c126b6b..1f3c75c5 100644 --- a/mfkdf2/src/definitions/entropy.rs +++ b/mfkdf2/src/definitions/entropy.rs @@ -1,6 +1,18 @@ +/// Entropy estimates for a single MFKDF2 factor. +/// +/// This type tracks how hard a factor is to guess, measured in bits of entropy. +/// +/// In general, `real` should be less than or equal to `theoretical`. Together they give a +/// practical and a theoretical view of factor's strength. +/// +/// 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)] pub struct MFKDF2Entropy { + /// Conservative estimate based on how the factor is actually produced or used. Calculated + /// using Dropbox's `zxcvbn` estimator. pub real: f64, + /// Upper-bound estimate of the factor's strength. pub theoretical: u32, } diff --git a/mfkdf2/src/definitions/factor.rs b/mfkdf2/src/definitions/factor.rs index 9a692c83..f57488b6 100644 --- a/mfkdf2/src/definitions/factor.rs +++ b/mfkdf2/src/definitions/factor.rs @@ -1,24 +1,82 @@ +//! # MFKDF2 Factor +//! +//! A Factor represents an authentication primitive. Each factor has: +//! +//! - **Factor material**: the secret input (e.g., a password, TOTP secret, hardware key seed) +//! - **Public state**: non-secret metadata the factor needs to operate (e.g., counters, +//! identifiers) use serde::{Deserialize, Serialize}; use crate::setup::factors::{hmacsha1, hotp, ooba, passkey, password, question, stack, totp, uuid}; +/// Trait for factor metadata. #[cfg_attr(feature = "bindings", uniffi::export)] -pub trait FactorMetadata: Send + Sync + std::fmt::Debug { +pub(crate) trait FactorMetadata: Send + Sync + std::fmt::Debug { + /// Returns the bytes of the factor material. fn bytes(&self) -> Vec; + /// Returns the type of the factor. fn kind(&self) -> String; } +/// MFKDF2 factor instance. +/// +/// In MFKDF2 protocol, a factor combines a secret piece of data (the factor material, often derived +/// from a password, hardware token response, TOTP code, etc.) with some public state stored on the +/// server. The job of a factor is to turn this dynamic user input into stable key material that can +/// be reused across multiple key derivations. +/// +/// Each factor has two core operations: +/// - `setup`: creates an initial factor instance from a factor-specific configuration (e.g., +/// password policy, TOTP parameters, hardware token IDs) +/// - `derive`: given a fresh user "witness" (e.g., the current password or OTP) and the current +/// public state, produces new factor material and updated public state for the next use. +/// +/// # Example +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::{FactorMetadata, FactorType}, +/// derive::factors::password as derive_password, +/// setup::factors::password::{PasswordOptions, password}, +/// }; +/// +/// // setup a password factor with id "pwd" +/// let setup = password("password123", PasswordOptions { id: Some("pwd".to_string()) })?; +/// +/// let p = match &setup.factor_type { +/// FactorType::Password(p) => p, +/// _ => panic!("Wrong factor type"), +/// }; +/// assert_eq!(p.password, "password123"); +/// assert_eq!(p.bytes(), "password123".as_bytes()); +/// +/// // derive a key using the password factor +/// let derive = derive_password("password123")?; +/// let p = match &derive.factor_type { +/// FactorType::Password(p) => p, +/// _ => panic!("Wrong factor type"), +/// }; +/// assert_eq!(p.password, "password123"); +/// assert_eq!(derive.data(), "password123".as_bytes()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Serialize, Deserialize)] pub struct MFKDF2Factor { + /// Optional application-defined identifier for the factor. pub id: Option, + /// Concrete factor implementation (password, TOTP, passkey, etc.). pub factor_type: FactorType, + /// Optional estimated real [`MFKDF2Entropy`](`crate::definitions::MFKDF2Entropy`) of this factor + /// instance (in bits). pub entropy: Option, } impl MFKDF2Factor { + /// Returns the type of the factor. pub fn kind(&self) -> String { self.factor_type.kind() } + /// Returns the bytes of the factor material. pub fn data(&self) -> Vec { self.factor_type.bytes() } } @@ -35,18 +93,33 @@ impl std::fmt::Debug for MFKDF2Factor { } } +/// Factor type enum representing all supported authentication factors. +/// +/// Each variant corresponds to a concrete factor type (such as password, TOTP, passkey, +/// etc.). Every factor implement the `FactorMetadata`, `FactorSetup`, and `FactorDerive` traits, +/// which define the common interface for factor management, setup, and derivation. #[cfg_attr(feature = "bindings", derive(uniffi::Enum))] #[derive(Clone, Debug, Serialize, Deserialize)] pub enum FactorType { + /// [`password::Password`] factor. Password(password::Password), + /// [`hotp::HOTP`] factor. HOTP(hotp::HOTP), + /// [`question::Question`] factor. Question(question::Question), + /// [`uuid::UUIDFactor`] factor. UUID(uuid::UUIDFactor), + /// [`hmacsha1::HmacSha1`] factor. HmacSha1(hmacsha1::HmacSha1), + /// [`totp::TOTP`] factor. TOTP(totp::TOTP), + /// [`ooba::Ooba`] factor. OOBA(ooba::Ooba), + /// [`passkey::Passkey`] factor. Passkey(passkey::Passkey), + /// [`stack::Stack`] factor. Stack(stack::Stack), + /// [Persisted](`crate::derive::factors::persisted::Persisted`) factor. Persisted(crate::derive::factors::persisted::Persisted), } diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs b/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs index 4d796af6..901cc7d3 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/crypto.rs @@ -1,4 +1,5 @@ impl crate::definitions::MFKDF2DerivedKey { + /// Returns an HKDF-SHA256 derived key for the given purpose and salt. pub fn get_subkey(&self, purpose: Option<&str>, salt: Option<&[u8]>) -> [u8; 32] { let salt = salt.unwrap_or(&[]); let purpose = purpose.unwrap_or(""); @@ -6,8 +7,9 @@ impl crate::definitions::MFKDF2DerivedKey { } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_get_subkey( +fn derived_key_get_subkey( derived_key: &crate::definitions::MFKDF2DerivedKey, purpose: Option, salt: Option>, diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs b/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs index 230ef5b0..0c113eaa 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/hints.rs @@ -1,3 +1,13 @@ +//! Probabilistic factor hints. +//! +//! This module implements the MFKDF2 "hints" feature. Hints allow you to +//! store a small number of bits of entropy derived from one or more input +//! factors directly in the public MFKDF2 policy. +//! +//! Storing a `b`-bit hint for a factor reduces the brute-force strength of the +//! final derived key by approximately `b` bits, but allows clients to +//! probabilistically validate whether a candidate factor is likely to be +//! correct before attempting a full derivation. use std::fmt::Write; use base64::engine::general_purpose; @@ -5,6 +15,23 @@ use base64::engine::general_purpose; use crate::{definitions::MFKDF2DerivedKey, error::MFKDF2Error}; impl MFKDF2DerivedKey { + /// Compute a probabilistic hint for a single factor. + /// + /// This function derives a deterministic bitstring from the secret material + /// backing the given factor and returns the least-significant `bits` of that + /// string as a binary `String` (e.g. `"0101010"`). These bits can be stored + /// in the public policy and later recomputed for candidate factors to + /// probabilistically validate whether they are correct. + /// + /// Storing a `b`-bit hint for a factor reduces the brute-force strength of + /// the final derived key by approximately `b` bits. + /// + /// # Errors + /// + /// - Returns [`MFKDF2Error::InvalidHintLength`] if `bits == 0`. + /// - Returns [`MFKDF2Error::MissingFactor`] if no factor with `factor_id` exists in the policy. + /// - Propagates any cryptographic or decoding errors encountered while deriving the factor + /// material. pub fn get_hint(&self, factor_id: &str, bits: u8) -> Result { if bits == 0 { return Err(MFKDF2Error::InvalidHintLength("bits must be greater than 0")); @@ -48,6 +75,65 @@ impl MFKDF2DerivedKey { ) } + /// Compute and store a probabilistic hint for a factor in the public policy. + /// + /// This is a convenience wrapper around [`MFKDF2DerivedKey::get_hint`] that + /// computes a hint and stores it on the matching factor within the + /// [Policy](`crate::policy::Policy`) associated with this + /// derived key. + /// + /// If `bits` is `None`, a default of `7` bits is used, which gives legitimate + /// users a greater than 99% chance of detecting an incorrect factor while + /// leaking only a negligible amount of information about the underlying + /// factor to most adversaries. + /// + /// # Examples + /// + /// Store a 7-bit hint for a password factor during setup and reuse it during + /// derivation: + /// + /// ```rust + /// use std::collections::HashMap; + /// + /// use mfkdf2::{ + /// definitions::MFKDF2Options, + /// derive, + /// error::MFKDF2Error, + /// setup::{ + /// self, + /// factors::{password, password::PasswordOptions}, + /// }, + /// }; + /// // Create a policy with a single password factor. + /// let mut setup_key = setup::key( + /// &[password("correct horse battery staple", PasswordOptions { + /// id: Some("password1".to_string()), + /// })?], + /// MFKDF2Options::default(), + /// )?; + /// + /// // Store a 7-bit hint for the password factor in the public policy. + /// setup_key.add_hint("password1", None)?; + /// + /// // Later, compute the expected hint for a candidate password. + /// let candidate_hint = setup_key.get_hint("password1", 7)?; + /// assert_eq!(candidate_hint.len(), 7); + /// + /// // During derivation, the library will automatically compare any stored + /// // hints and return `MFKDF2Error::HintMismatch` if they do not match. + /// let derived_key = derive::key( + /// &setup_key.policy, + /// HashMap::from([( + /// "password1".to_string(), + /// derive::factors::password("correct horse battery staple")?, + /// )]), + /// true, // integrity + /// false, // allow_partial + /// )?; + /// + /// assert_eq!(derived_key.key, setup_key.key); + /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) + /// ``` pub fn add_hint(&mut self, factor_id: &str, bits: Option) -> Result<(), MFKDF2Error> { let bits = bits.unwrap_or(7); let hint = self.get_hint(factor_id, bits); @@ -57,8 +143,9 @@ impl MFKDF2DerivedKey { } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_get_hint( +fn derived_key_get_hint( derived_key: &MFKDF2DerivedKey, factor_id: &str, bits: u8, @@ -66,8 +153,9 @@ pub fn derived_key_get_hint( derived_key.get_hint(factor_id, bits) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_add_hint( +fn derived_key_add_hint( derived_key: MFKDF2DerivedKey, factor_id: &str, bits: Option, @@ -82,10 +170,11 @@ mod tests { use std::collections::HashMap; use crate::{ + definitions::MFKDF2Options, derive, derive::factors as derive_factors, error, - setup::{self, factors::password::PasswordOptions, key::MFKDF2Options}, + setup::{self, factors::password::PasswordOptions}, }; #[test] diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/mfdpg.rs b/mfkdf2/src/definitions/mfkdf_derived_key/mfdpg.rs index e01687c5..7edb09ec 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/mfdpg.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/mfdpg.rs @@ -1,23 +1,86 @@ +//! Multi-factor deterministic password generator (MFDPG), which aims to rectify the shortcomings +//! of existing deterministic password generators (DPGs) by incorporating multi-factor key +//! derivation into the password management process. +//! +//! This module implements the MFDPG primitive on top of `MFKDF2DerivedKey` by sampling passwords +//! from a caller-provided regular language using deterministic randomness derived from the key. +//! +//! Generated passwords behave like a deterministic password manager: given the same multi-factor +//! setup, purpose string, salt, and policy regex, the derived password remains stable across +//! sessions, platforms, and bindings. use rand::{SeedableRng, distributions::Distribution}; use rand_regex::Regex; +use crate::error::MFKDF2Error; + impl crate::definitions::MFKDF2DerivedKey { - pub fn derive_password(&self, purpose: Option<&str>, salt: Option<&[u8]>, regex: &str) -> String { + /// Derives a deterministic, policy-compliant password from an `MFKDF2DerivedKey` + /// + /// The password depends on the derived key material, an optional purpose string, an optional + /// salt, and a caller-supplied regular expression that describes the allowed password language + /// + /// # Arguments + /// + /// * `purpose`: Optional logical namespace such as a domain, account identifier, or resource + /// label + /// * `salt`: Optional opaque salt slice; changing this parameter changes the derived password + /// even under the same purpose + /// * `regex`: Regular expression understood by `rand_regex` that constrains the generated + /// password shape + /// + /// # Examples + /// + /// The following example mirrors the MFDPG JavaScript helper where the same inputs produce the + /// same password string + /// + /// ```rust + /// # use mfkdf2::{setup, setup::factors::password::PasswordOptions}; + /// # + /// let setup_key = setup::key( + /// &[setup::factors::password("password1", PasswordOptions { id: Some("password1".to_owned()) })?], + /// mfkdf2::definitions::MFKDF2Options::default(), + /// )?; + /// + /// let password = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; + /// + /// let password2 = + /// setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; + /// + /// assert_eq!(password, password2); + /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) + /// ``` + /// + /// # Determinism + /// + /// Repeated calls with the same key, purpose, salt, and regular expression always yield the same + /// password + /// + /// # Errors + /// + /// - [`MFKDF2Error::Regex`](`crate::error::MFKDF2Error::Regex`) if the regular expression is + /// invalid + pub fn derive_password( + &self, + purpose: Option<&str>, + salt: Option<&[u8]>, + regex: &str, + ) -> Result { let password_key = self.get_subkey(purpose, salt); // seed and rng with password_key let mut rng = rand_chacha::ChaCha20Rng::from_seed(password_key); - let dfa = Regex::compile(regex, 10000).expect("Failed to compile regex"); - dfa.sample(&mut rng) + let dfa = Regex::compile(regex, 10000)?; + Ok(dfa.sample(&mut rng)) } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_derive_password( +fn derived_key_derive_password( derived_key: &crate::definitions::MFKDF2DerivedKey, purpose: Option, salt: Option>, regex: &str, -) -> String { +) -> Result { let purpose = purpose.as_deref(); let salt = salt.as_deref(); derived_key.derive_password(purpose, salt, regex) @@ -28,6 +91,7 @@ mod tests { use std::collections::HashMap; use crate::{ + definitions::MFKDF2Options, derive, error, setup::{self, factors::password::PasswordOptions}, }; @@ -38,14 +102,16 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; - let password = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}"); + let password = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}")?; assert!(password.len() > 5 && password.len() < 11); assert!(password.chars().all(|c| c.is_alphanumeric())); - let password2 = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}"); + let password2 = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}")?; assert_eq!(password, password2); let derive_factors = @@ -54,7 +120,7 @@ mod tests { assert_eq!(derive_key.key, setup_key.key); let password3 = - derive_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}"); + derive_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z0-9]{8}")?; assert_eq!(password, password3); Ok(()) @@ -66,7 +132,7 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; // Complex regex pattern: ([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]* @@ -74,7 +140,7 @@ mod tests { Some("example.com"), Some(b"salt"), "([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]*", - ); + )?; let derive_factors = HashMap::from([("password1".to_string(), derive::factors::password("password1")?)]); @@ -84,7 +150,7 @@ mod tests { Some("example.com"), Some(b"salt"), "([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]*", - ); + )?; assert_eq!(password1, password3); Ok(()) @@ -96,12 +162,14 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; - let password1 = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + let password1 = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; - let password2 = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + let password2 = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; assert_eq!(password1, password2); assert!(password1.len() >= 6 && password1.len() <= 10); @@ -115,17 +183,18 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; - let password1 = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + let password1 = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; let derive_factors = HashMap::from([("password1".to_string(), derive::factors::password("password1")?)]); let derive_key = derive::key(&setup_key.policy, derive_factors, false, false)?; let password2 = - derive_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + derive_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; assert_eq!(password1, password2); assert!(password1.len() >= 6 && password1.len() <= 10); @@ -139,20 +208,20 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; let setup_key2 = setup::key( &[crate::setup::factors::password("password2", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; let password1 = - setup_key1.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + setup_key1.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; let password2 = - setup_key2.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + setup_key2.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; // Different setups should produce different passwords assert_ne!(password1, password2); @@ -165,10 +234,11 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; - let password1 = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + let password1 = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; let policy = setup_key.policy.clone(); let derive_factors1 = @@ -176,7 +246,7 @@ mod tests { let derive_key1 = derive::key(&policy, derive_factors1, false, false)?; let password2 = - derive_key1.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + derive_key1.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; // Same password should produce same result assert_eq!(password1, password2); @@ -186,7 +256,7 @@ mod tests { let derive_key2 = derive::key(&policy, derive_factors2, false, false)?; let password3 = - derive_key2.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + derive_key2.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; // Different password should produce different result assert_ne!(password1, password3); @@ -199,10 +269,11 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; - let password = setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}"); + let password = + setup_key.derive_password(Some("example.com"), Some(b"salt"), "[a-zA-Z]{6,10}")?; // Verify password length is within expected range assert!(password.len() > 5, "Password length should be above 5, got {}", password.len()); @@ -229,12 +300,12 @@ mod tests { &[crate::setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()), })?], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), )?; // Complex regex pattern: ([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]* let regex_pattern = "([A-Za-z]+[0-9]|[0-9]+[A-Za-z])[A-Za-z0-9]*"; - let password = setup_key.derive_password(Some("example.com"), Some(b"salt"), regex_pattern); + let password = setup_key.derive_password(Some("example.com"), Some(b"salt"), regex_pattern)?; // Verify password matches the complex regex pattern // The pattern requires either: diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs index 333346f3..8827d5ac 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/mod.rs @@ -1,3 +1,14 @@ +//! # MFKDF2 Derived Key +//! +//! The security properties of MFKDF allow the derived key to itself be used to authenticate end +//! users and implicitly verify all of their authentication factors. Because a properly-configured +//! MFKDF key (`K`) cannot feasibly be derived without the presentation of all constituent factors, +//! verifying a user’s derivation of `K` effectively constitutes verification of all factors. +//! +//! In practice, this means `MFKDF2DerivedKey` can safely serve as a high-entropy application root +//! 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. use std::collections::HashMap; use base64::{Engine, engine::general_purpose}; @@ -9,21 +20,37 @@ use crate::{ policy::Policy, }; -pub mod crypto; +mod crypto; pub mod hints; pub mod mfdpg; -pub mod persistence; +mod persistence; pub mod reconstitution; -pub mod strengthening; +mod strengthening; +/// MFKDF2 Derived key after the setup or derive operation. +/// +/// An [`MFKDF2DerivedKey`] bundles the static derived key material together with the resolved +/// [`Policy`] and auxiliary metadata needed for threshold recovery and factor management. +/// It is produced by the MFKDF2 setup and derive algorithms (see [`crate::setup::key`] and +/// [`crate::derive::key`]). #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct MFKDF2DerivedKey { + /// Authentication policy describing factors, threshold, and integrity configuration associated + /// with this key. pub policy: Policy, + /// 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, + /// Shamir‑style shares of `secret`, one per factor, used by reconstitution and + /// threshold‑management routines. pub shares: Vec>, + /// Per‑factor public outputs produced during setup or derive (such as strength metrics or + /// factor‑specific metadata). pub outputs: HashMap, + /// Measured and theoretical entropy estimates for the derived key, useful for auditing and + /// security analysis. pub entropy: MFKDF2Entropy, } diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/persistence.rs b/mfkdf2/src/definitions/mfkdf_derived_key/persistence.rs index 0c61a208..1442683c 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/persistence.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/persistence.rs @@ -1,12 +1,33 @@ impl crate::definitions::MFKDF2DerivedKey { + /// Persistence allows you to save one or more of the factors used to setup a multi-factor derived + /// key (eg. as browser cookies) so that they do not need to be used to derive the key in the + /// future. + /// + /// # Example + /// + /// ```rust + /// use mfkdf2::{ + /// definitions::MFKDF2Options, + /// error::MFKDF2Error, + /// setup::{ + /// self, + /// factors::{password, password::PasswordOptions}, + /// }, + /// }; + /// let setup_key = + /// setup::key(&[password("password", PasswordOptions::default())?], MFKDF2Options::default())?; + /// let share = setup_key.persist_factor("password"); + /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) + /// ``` pub fn persist_factor(&self, id: &str) -> Vec { let index = self.policy.factors.iter().position(|f| f.id == id).unwrap(); self.shares[index].clone() } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_persist_factor( +fn derived_key_persist_factor( derived_key: &crate::definitions::MFKDF2DerivedKey, id: &str, ) -> Vec { diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs b/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs index c49ea192..a5c4eb4d 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/reconstitution.rs @@ -1,3 +1,101 @@ +//! # MFKDF2 Factor Recovery +//! +//! Reconstitution refers to the process of modifying the factors used to derive a key without +//! changing the value of the derived key. +//! +//! Consider a key derived from a password, a TOTP factor, and a UUID factor. Using threshold +//! recovery, the user can derive the key with only a subset of factors inside the policy. +//! +//! ```rust +//! # use std::collections::HashMap; +//! # use uuid::Uuid; +//! # use mfkdf2::{ +//! # setup, +//! # setup::{ +//! # factors::{password::PasswordOptions, totp::TOTPOptions, uuid::UUIDOptions}, +//! # }, +//! # definitions::MFKDF2Options, +//! # }; +//! # +//! let uuid = Uuid::parse_str("f9bf78b9-54e7-4696-97dc-5e750de4c592").unwrap(); +//! let setup_factors = vec![ +//! setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()) })?, +//! setup::factors::totp(TOTPOptions { +//! id: Some("totp1".to_string()), +//! ..Default::default() +//! })?, +//! setup::factors::uuid(UUIDOptions { +//! id: Some("uuid1".to_string()), +//! uuid: Some(uuid), +//! })?, +//! ]; +//! let mut setup_key = setup::key(&setup_factors, MFKDF2Options::default())?; +//! +//! // Let's say now the user wishes to reset the password. The `MFKDF2DerivedKey` can be updated to reflect the new password like so: +//! setup_key.recover_factor(setup::factors::password("newPassword1", PasswordOptions { +//! id: Some("password1".to_string()), +//! })?); +//! +//! Ok::<(), mfkdf2::error::MFKDF2Error>(()) +//! ``` +//! +//! The key can now be derived with the modified credentials: +//! ```rust +//! # use std::collections::HashMap; +//! # use uuid::Uuid; +//! # use std::time::{SystemTime, UNIX_EPOCH}; +//! # use mfkdf2::{ +//! # derive, +//! # derive::factors::{ +//! # password as derive_password, totp as derive_totp, uuid as derive_uuid, +//! # }, +//! # setup, +//! # setup::{ +//! # factors::{password::PasswordOptions, totp::TOTPOptions, uuid::UUIDOptions}, +//! # }, +//! # definitions::MFKDF2Options, +//! # otpauth::HashAlgorithm, +//! # }; +//! # +//! # let uuid = Uuid::parse_str("f9bf78b9-54e7-4696-97dc-5e750de4c592").unwrap(); +//! # let setup_factors = vec![ +//! # setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()) })?, +//! # setup::factors::totp(TOTPOptions { id: Some("totp1".to_string()), ..Default::default() })?, +//! # setup::factors::uuid(UUIDOptions { id: Some("uuid1".to_string()), uuid: Some(uuid) })?, +//! # ]; +//! # let secret = if let mfkdf2::definitions::FactorType::TOTP(ref f) = setup_factors[1].factor_type { +//! # f.config.secret.clone() +//! # } else { +//! # unreachable!() +//! # }; +//! # let mut setup_key = setup::key(&setup_factors, MFKDF2Options::default())?; +//! +//! # setup_key.recover_factor(setup::factors::password("newPassword1", PasswordOptions { +//! # id: Some("password1".to_string()), +//! # })?); +//! +//! # let step = 30; +//! # let digits = 6; +//! # let hash = HashAlgorithm::Sha1; +//! # let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; +//! # let counter = now_ms / (step * 1000); +//! # let code = mfkdf2::otpauth::generate_otp_token(&secret[..20], counter, &hash, digits); +//! +//! let mut derived_key = derive::key( +//! &setup_key.policy, +//! HashMap::from([ +//! ("password1".to_string(), derive::factors::password("newPassword1")?), +//! ("totp1".to_string(), derive::factors::totp(code, None)?), +//! ("uuid1".to_string(), derive::factors::uuid(uuid)?), +//! ]), +//! true, +//! false, +//! )?; +//! +//! assert_eq!(derived_key.key, setup_key.key); +//! Ok::<(), mfkdf2::error::MFKDF2Error>(()) +//! ``` + use std::collections::{BTreeMap, HashSet}; use base64::{Engine, engine::general_purpose}; @@ -7,38 +105,47 @@ use crate::{ crypto::{encrypt, hkdf_sha256_with_info, hmacsha256}, definitions::{MFKDF2DerivedKey, MFKDF2Factor}, error::{MFKDF2Error, MFKDF2Result}, - setup::{FactorSetup, key::PolicyFactor}, + policy::PolicyFactor, + setup::FactorSetup, }; impl MFKDF2DerivedKey { + /// Sets a new threshold for the key. pub fn set_threshold(&mut self, threshold: u8) -> MFKDF2Result<()> { self.reconstitute(&[], &[], Some(threshold)) } + /// Removes a factor from the key. pub fn remove_factor(&mut self, factor: &str) -> MFKDF2Result<()> { self.reconstitute(&[factor], &[], None) } + /// Removes multiple factors from the key. pub fn remove_factors(&mut self, factors: &[&str]) -> MFKDF2Result<()> { self.reconstitute(factors, &[], None) } + /// Adds a factor to the key. pub fn add_factor(&mut self, factor: MFKDF2Factor) -> MFKDF2Result<()> { self.reconstitute(&[], &[factor], None) } + /// Adds multiple factors to the key. pub fn add_factors(&mut self, factors: &[MFKDF2Factor]) -> MFKDF2Result<()> { self.reconstitute(&[], factors, None) } + /// Recovers a factor from the key. pub fn recover_factor(&mut self, factor: MFKDF2Factor) -> MFKDF2Result<()> { self.reconstitute(&[], &[factor], None) } + /// Recovers multiple factors from the key. pub fn recover_factors(&mut self, factors: &[MFKDF2Factor]) -> MFKDF2Result<()> { self.reconstitute(&[], factors, None) } + /// Reconstitutes the key with the given factors. pub fn reconstitute( &mut self, remove_factor: &[&str], @@ -66,7 +173,7 @@ impl MFKDF2DerivedKey { material.insert( factor.id.as_str(), - factor_material.try_into().map_err(|_| MFKDF2Error::TryFromVecError)?, + factor_material.try_into().map_err(|_| MFKDF2Error::TryFromVec)?, ); } @@ -142,7 +249,7 @@ impl MFKDF2DerivedKey { format!("mfkdf2:factor:pad:{}", factor.id).as_bytes(), ) } else { - return Err(MFKDF2Error::TryFromVecError); + return Err(MFKDF2Error::TryFromVec); }; let secret_key = hkdf_sha256_with_info( @@ -174,8 +281,9 @@ impl MFKDF2DerivedKey { } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_set_threshold( +fn derived_key_set_threshold( derived_key: MFKDF2DerivedKey, threshold: u8, ) -> MFKDF2Result { @@ -184,8 +292,9 @@ pub fn derived_key_set_threshold( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_remove_factor( +fn derived_key_remove_factor( derived_key: MFKDF2DerivedKey, factor: &str, ) -> MFKDF2Result { @@ -194,8 +303,9 @@ pub fn derived_key_remove_factor( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_remove_factors( +fn derived_key_remove_factors( derived_key: MFKDF2DerivedKey, factors: &[String], ) -> MFKDF2Result { @@ -204,8 +314,9 @@ pub fn derived_key_remove_factors( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_add_factor( +fn derived_key_add_factor( derived_key: MFKDF2DerivedKey, factor: MFKDF2Factor, ) -> MFKDF2Result { @@ -214,8 +325,9 @@ pub fn derived_key_add_factor( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_add_factors( +fn derived_key_add_factors( derived_key: MFKDF2DerivedKey, factors: &[MFKDF2Factor], ) -> MFKDF2Result { @@ -224,8 +336,9 @@ pub fn derived_key_add_factors( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_recover_factor( +fn derived_key_recover_factor( derived_key: MFKDF2DerivedKey, factor: MFKDF2Factor, ) -> MFKDF2Result { @@ -234,8 +347,9 @@ pub fn derived_key_recover_factor( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_recover_factors( +fn derived_key_recover_factors( derived_key: MFKDF2DerivedKey, factors: &[MFKDF2Factor], ) -> MFKDF2Result { @@ -244,8 +358,9 @@ pub fn derived_key_recover_factors( Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_reconstitute( +fn derived_key_reconstitute( derived_key: MFKDF2DerivedKey, remove_factor: &[String], add_factor: &[MFKDF2Factor], @@ -265,8 +380,10 @@ mod tests { use std::collections::HashMap; use crate::{ - derive, derive::factors as derive_factors, error, setup, - setup::factors::password::PasswordOptions, + definitions::MFKDF2Options, + derive::{self, factors as derive_factors}, + error, + setup::{self, factors::password::PasswordOptions}, }; #[test] @@ -286,7 +403,7 @@ mod tests { })?, ]; - let mut setup = setup::key(&setup_factors, setup::key::MFKDF2Options { + let mut setup = setup::key(&setup_factors, MFKDF2Options { threshold: Some(3), integrity: Some(false), ..Default::default() @@ -324,7 +441,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -409,7 +526,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -473,7 +590,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -506,7 +623,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -547,7 +664,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -584,7 +701,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -625,7 +742,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(3), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -665,7 +782,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let key = setup_key.key.clone(); @@ -699,7 +816,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(3), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let result = setup_key.reconstitute( @@ -729,7 +846,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(3), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let result = setup_key.reconstitute( @@ -764,7 +881,7 @@ mod tests { })?, ]; - let options = setup::key::MFKDF2Options { threshold: Some(3), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; let mut setup_key = setup::key(&setup_factors, options)?; let result = setup_key.reconstitute(&["password1", "password2", "password3"], &[], Some(4)); diff --git a/mfkdf2/src/definitions/mfkdf_derived_key/strengthening.rs b/mfkdf2/src/definitions/mfkdf_derived_key/strengthening.rs index 2344bd41..b526255a 100644 --- a/mfkdf2/src/definitions/mfkdf_derived_key/strengthening.rs +++ b/mfkdf2/src/definitions/mfkdf_derived_key/strengthening.rs @@ -4,6 +4,94 @@ use base64::{Engine, engine::general_purpose}; use crate::{crypto::encrypt, definitions::MFKDF2DerivedKey, error::MFKDF2Result}; impl MFKDF2DerivedKey { + /// Increase the Argon2 work factor for an already derived key **without** changing the key. + /// + /// Strengthening is useful when you want to upgrade the KDF parameters over time (e.g. as + /// hardware gets faster) but you cannot afford to re-encrypt all application data. The + /// [`MFKDF2DerivedKey`] already contains the user-derived key and an authenticated copy of + /// the public policy; calling this method: + /// + /// - derives a new key-encapsulation key (KEK) from `self.secret` and the policy salt using + /// stronger Argon2 parameters, and + /// - re-encrypts the policy key with that KEK and updates `policy.time` / `policy.memory`. + /// + /// Clients should persist the updated `policy` back to their storage (e.g. user database) + /// and discard the old one. Any attempt to reuse or roll back the previous policy will fail + /// the integrity check during the next derive + /// [`MFKDF2Error::PolicyIntegrityCheckFailed`](`crate::error::MFKDF2Error::PolicyIntegrityCheckFailed`), + /// ensuring that only a user who can compute the correct key can authorize an increase in cost. + /// + /// The `time` and `memory` arguments are additive deltas over the library defaults used at + /// setup time. Internally they are applied as `DEFAULT_T_COST + time` and + /// `DEFAULT_M_COST + memory`, which allows you to express "make this policy 3 steps slower + /// and 16 MiB more memory hungry than it was originally", rather than hard-coding + /// absolute Argon2 parameters. + /// + /// # Example + /// + /// ```rust + /// use std::collections::HashMap; + /// use mfkdf2::{ + /// derive, + /// derive::factors as derive_factors, + /// error::MFKDF2Error, + /// setup::{ + /// self, + /// factors::password::PasswordOptions, + /// }, + /// definitions::MFKDF2Options, + /// }; + /// + /// // 1. Create a simple single-password policy + /// let setup_factors = vec![ + /// setup::factors::password("password1", PasswordOptions::default())?, + /// ]; + /// + /// let setup_key = + /// setup::key(&setup_factors, MFKDF2Options::default())?; + /// + /// // 2. User logs in with the same password and we derive the current key. + /// let mut derived_key = derive::key( + /// &setup_key.policy, + /// HashMap::from([( + /// "password".to_string(), + /// derive_factors::password("password1").expect("Failed to derive password factor"), + /// )]), + /// true, // use integrity check + /// false, // use stack key + /// )?; + /// + /// // 3. bump the Argon2 time and memory costs. These values are *deltas* over the defaults from setup. + /// derived_key.strengthen(3, 16 * 1024)?; + /// + /// // 4. Persist `derived_key.policy` as the new policy for this user. + /// // Future derives must use the strengthened policy. + /// let derived_key2 = derive::key( + /// &derived_key.policy, + /// HashMap::from([( + /// "password".to_string(), + /// derive_factors::password("password1")?, + /// )]), + /// true, + /// false, + /// )?; + /// + /// assert_eq!(derived_key2.key, setup_key.key); + /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) + /// ``` + /// + /// # Errors + /// + /// This function returns an [`MFKDF2Result`] whose error is typically: + /// + /// - [`crate::error::MFKDF2Error::Argon2`] if the chosen `time` / `memory` values cannot be + /// represented as valid Argon2 parameters. + /// - [`crate::error::MFKDF2Error::Base64Decode`] if the policy salt has been corrupted or + /// tampered with and can no longer be base64-decoded. + /// + /// Note that downstream operations that use the upgraded policy may return + /// [`crate::error::MFKDF2Error::PolicyIntegrityCheckFailed`] if an attacker attempts to + /// weaken or roll back the policy, as that invalidates the integrity MAC. pub fn strengthen(&mut self, time: u32, memory: u32) -> MFKDF2Result<()> { self.policy.time = time; self.policy.memory = memory; @@ -30,8 +118,9 @@ impl MFKDF2DerivedKey { } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derived_key_strengthen( +fn derived_key_strengthen( derived_key: MFKDF2DerivedKey, time: u32, memory: u32, @@ -46,10 +135,11 @@ mod tests { use std::collections::HashMap; use crate::{ + definitions::MFKDF2Options, derive, derive::factors as derive_factors, error, - setup::{self, factors::password::PasswordOptions, key::MFKDF2Options}, + setup::{self, factors::password::PasswordOptions}, }; #[test] diff --git a/mfkdf2/src/definitions/mod.rs b/mfkdf2/src/definitions/mod.rs index 2777636a..6a00ed33 100644 --- a/mfkdf2/src/definitions/mod.rs +++ b/mfkdf2/src/definitions/mod.rs @@ -1,11 +1,64 @@ -pub mod bytearray; -pub mod mfkdf_derived_key; -#[cfg(feature = "bindings")] pub mod uniffi_types; - -pub mod entropy; +//! # MFKDF2 Definitions +//! +//! This module contains the definitions for the MFKDF2 protocol. +//! +//! - [`MFKDF2Options`]: Options for setting up a [`MFKDF2DerivedKey`] key. +//! - [`MFKDF2Entropy`]: Entropy estimation for the derived key. +//! - [`MFKDF2Factor`]: Factor type. +//! - [`MFKDF2DerivedKey`]: Derived key after setup or derive operation. +//! - [`FactorType`]: Factor type enum. +//! - [`ByteArray`]: Generic byte array type. +//! - [`Key`]: 32 byte key. +//! - [`Salt`]: 32 byte salt. +mod bytearray; +mod entropy; pub mod factor; +pub mod mfkdf_derived_key; +#[cfg(feature = "bindings")] mod uniffi_types; pub use bytearray::{ByteArray, Key, Salt}; pub use entropy::MFKDF2Entropy; -pub use factor::{FactorMetadata, FactorType, MFKDF2Factor}; +pub use factor::{FactorType, MFKDF2Factor}; pub use mfkdf_derived_key::MFKDF2DerivedKey; +use serde::{Deserialize, Serialize}; + +/// Options for setting up a [`MFKDF2DerivedKey`] key. +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +#[derive(Clone, Serialize, Deserialize)] +pub struct MFKDF2Options { + /// ID of the policy. If not provided, a random UUID will be generated. + pub id: Option, + /// Threshold number of factors needed to derive the key. + /// Minimum number of factors is 1, maximum is the number of factors provided. + pub threshold: Option, + /// 32 byte salt for key derivation. If not provided, a random salt will be generated. + pub salt: Option, + /// Flag to use a stack key for key derivation. + pub stack: Option, + /// Flag to perform integrity checks for the policy. + /// Default is true. + pub integrity: Option, + /// Additional time cost for argon2id key derivation. + /// Default is 0. + pub time: Option, + /// Additional memory cost for argon2id key derivation. + /// Default is 0. + pub memory: Option, +} + +impl Default for MFKDF2Options { + fn default() -> Self { + let mut salt = [0u8; 32]; + crate::rng::fill_bytes(&mut salt); + + Self { + id: Some(uuid::Uuid::new_v4().to_string()), + threshold: None, + salt: Some(salt.into()), + stack: None, + integrity: Some(true), + time: Some(0), + memory: Some(0), + } + } +} diff --git a/mfkdf2/src/definitions/uniffi_types.rs b/mfkdf2/src/definitions/uniffi_types.rs index 4639340e..278733ed 100644 --- a/mfkdf2/src/definitions/uniffi_types.rs +++ b/mfkdf2/src/definitions/uniffi_types.rs @@ -1,3 +1,6 @@ +//! # Uniffi Types +//! +//! This module contains the Uniffi custom types for the MFKDF2 library. use jsonwebtoken::jwk::Jwk; use serde_json::Value; use uuid::Uuid; @@ -40,5 +43,4 @@ uniffi::custom_type!(Value, String, { try_lift: |s: String| Ok(serde_json::from_str(&s)?), }); -// Uniffi custom type for Key uniffi::custom_type!(Key, Vec); diff --git a/mfkdf2/src/derive/factors/hmacsha1.rs b/mfkdf2/src/derive/factors/hmacsha1.rs index 8fb641e8..725f3040 100644 --- a/mfkdf2/src/derive/factors/hmacsha1.rs +++ b/mfkdf2/src/derive/factors/hmacsha1.rs @@ -1,3 +1,10 @@ +//! Factor construction derive phase for the HMAC‑SHA1 factor from +//! [`HMAC-SHA1`](`crate::setup::factors::hmacsha1()`). +//! +//! - During setup, the factor stores a padded HMAC key and a challenge in the policy. +//! - During derive, this module consumes an HMAC response over that challenge and reconstructs the +//! same padded secret so that the factor contributes identical bytes to the MFKDF2 key +//! derivation. use serde_json::{Value, json}; use crate::{ @@ -12,6 +19,7 @@ impl FactorDerive for HmacSha1 { type Output = Value; type Params = Value; + /// 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()); @@ -34,6 +42,7 @@ impl FactorDerive for HmacSha1 { Ok(()) } + /// Computes a new challenge and encrypts the secret material as pad for the factor. fn params(&self, _key: Key) -> MFKDF2Result { let mut challenge = [0u8; 64]; crate::rng::fill_bytes(&mut challenge); @@ -56,6 +65,71 @@ impl FactorDerive for HmacSha1 { } } +/// Factor construction derive phase for an HMAC‑SHA1 factor +/// +/// The caller is expected to compute `response = HMAC-SHA1(secret, challenge)` using the secret +/// key material stored by the application and the `challenge` value provided in the setup policy +/// parameters. This helper wraps the response in an [`MFKDF2Factor`] witness Wᵢⱼ that, once +/// combined with the policy, recovers the same padded secret as in setup. +/// +/// # Errors +/// +/// - [`MFKDF2Error::MissingDeriveParams`](`crate::error::MFKDF2Error::MissingDeriveParams`) if the +/// setup policy omits the `"pad"` parameter when `include_params` is invoked +/// - [`MFKDF2Error::InvalidDeriveParams`](`crate::error::MFKDF2Error::InvalidDeriveParams`) if the +/// `"pad"` field is not valid hex or has an unexpected shape +/// +/// # Example +/// +/// Single‑factor setup and factor construction derive phase using the HMAC‑SHA1 factor within +/// `KeySetup`/`KeyDerive`: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::hmacsha1::{HmacSha1Options}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # }; +/// # use hmac::{Mac, Hmac}; +/// # use sha1::Sha1; +/// # const HMACSHA1_SECRET: [u8; 20] = [0x11; 20]; +/// // KeySetup: build a policy with a single HMAC‑SHA1 factor +/// let setup_factor = setup::factors::hmacsha1(HmacSha1Options { +/// secret: Some(HMACSHA1_SECRET.to_vec()), +/// ..Default::default() +/// })?; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// // Read the challenge for this factor from the policy +/// let policy_factor = setup_key.policy.factors.iter().find(|f| f.id == "hmacsha1").unwrap(); +/// let setup_params = &policy_factor.params; +/// let challenge = hex::decode(setup_params["challenge"].as_str().unwrap()).unwrap(); +/// +/// // The hardware token (or equivalent) computes HMAC-SHA1 over the challenge +/// let response: [u8; 20] = as Mac>::new_from_slice(&HMACSHA1_SECRET) +/// .unwrap() +/// .chain_update(&challenge) +/// .finalize() +/// .into_bytes() +/// .into(); +/// +/// // Build the derive‑time HMAC witness and run KeyDerive +/// let derive_factor = derive::factors::hmacsha1(response.into())?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("hmacsha1".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn hmacsha1(response: HmacSha1Response) -> MFKDF2Result { Ok(MFKDF2Factor { id: None, @@ -68,8 +142,9 @@ pub fn hmacsha1(response: HmacSha1Response) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_hmacsha1(response: HmacSha1Response) -> MFKDF2Result { +async fn derive_hmacsha1(response: HmacSha1Response) -> MFKDF2Result { crate::derive::factors::hmacsha1(response) } diff --git a/mfkdf2/src/derive/factors/hotp.rs b/mfkdf2/src/derive/factors/hotp.rs index 118e05c8..fc025957 100644 --- a/mfkdf2/src/derive/factors/hotp.rs +++ b/mfkdf2/src/derive/factors/hotp.rs @@ -1,3 +1,10 @@ +//! This module constructs [`MFKDF2Factor`] witnesses Wᵢⱼ for the derive phase corresponding +//! to the setup factors defined in [hotp](`crate::setup::factors::hotp()`). +//! - During setup, the HOTP factor chooses a secret target code and encodes an offset and encrypted +//! pad into the policy; +//! - During derive, this module consumes the HOTP code Wᵢⱼ and reconstructs the same target value +//! using the stored offset so that the factor contributes stable material to the key derivation +//! while remaining backward‑compatible with existing OATH HOTP applications. use base64::Engine; use serde_json::{Value, json}; @@ -6,7 +13,7 @@ use crate::{ definitions::{FactorType, Key, MFKDF2Factor}, derive::FactorDerive, error::MFKDF2Result, - otpauth::generate_hotp_code, + otpauth::generate_otp_token, setup::factors::hotp::{HOTP, HOTPConfig, HOTPParams, mod_positive}, }; @@ -14,6 +21,7 @@ impl FactorDerive for HOTP { type Output = Value; type Params = Value; + /// Includes the public parameters for in factor state and calculates the target value. fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> { // Store the policy parameters for derive phase self.params = params.clone(); @@ -28,6 +36,7 @@ impl FactorDerive for HOTP { Ok(()) } + /// Decrypts the secret and generates a new HOTP code with incremented counter. fn params(&self, key: Key) -> MFKDF2Result { let params: HOTPParams = serde_json::from_value(self.params.clone())?; @@ -38,7 +47,7 @@ impl FactorDerive for HOTP { // Generate HOTP code with incremented counter let counter = params.counter + 1; let generated_code = - generate_hotp_code(&padded_secret[..20], counter, ¶ms.hash, params.digits); + generate_otp_token(&padded_secret[..20], counter, ¶ms.hash, params.digits); // Calculate new offset let new_offset = @@ -54,6 +63,71 @@ impl FactorDerive for HOTP { } } +/// HOTP factor construction derive phase +/// +/// The code should be the numeric one‑time password displayed by an authenticator app that has +/// been paired with the HOTP secret configured during setup. +/// +/// # Errors +/// +/// - [`MFKDF2Error::Serialize`](`crate::error::MFKDF2Error::Serialize`) if the stored policy +/// parameters cannot be decoded into [`HOTPParams`](`crate::setup::factors::hotp::HOTPParams`) +/// (for example, missing or malformed fields) +/// +/// # Example +/// +/// Single‑factor setup/derive using HOTP within `KeySetup`/`KeyDerive`: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # otpauth::HashAlgorithm, +/// # setup::{ +/// # self, +/// # factors::hotp::{HOTPOptions}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # }; +/// let secret = b"hello world mfkdf2!!".to_vec(); +/// let options = HOTPOptions { +/// id: Some("hotp".to_string()), +/// secret: Some(secret), +/// digits: Some(6), +/// hash: Some(HashAlgorithm::Sha1), +/// ..Default::default() +/// }; +/// +/// let setup_factor = setup::factors::hotp(options)?; +/// let hotp = if let mfkdf2::definitions::FactorType::HOTP(ref h) = setup_factor.factor_type { +/// h.clone() +/// } else { +/// unreachable!() +/// }; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// let policy_factor = setup_key.policy.factors.iter().find(|f| f.id == "hotp").unwrap(); +/// let params = &policy_factor.params; +/// let counter = params["counter"].as_u64().unwrap(); +/// let code = mfkdf2::otpauth::generate_otp_token( +/// &hotp.config.secret[..20], +/// counter, +/// &hotp.config.hash, +/// hotp.config.digits, +/// ); +/// +/// let derive_factor = derive::factors::hotp(code)?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("hotp".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn hotp(code: u32) -> MFKDF2Result { // Create HOTP factor with the user-provided code // The target will be calculated in include_params once we have the policy parameters @@ -69,8 +143,9 @@ pub fn hotp(code: u32) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_hotp(code: u32) -> MFKDF2Result { hotp(code) } +async fn derive_hotp(code: u32) -> MFKDF2Result { hotp(code) } #[cfg(test)] mod tests { @@ -107,7 +182,7 @@ mod tests { // Generate the correct HOTP code that the user would need to provide let correct_code = - generate_hotp_code(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); + generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); let expected_target = u32::from_be_bytes(factor.data().try_into().unwrap()); // Verify the relationship: target = (offset + correct_code) % 10^digits @@ -165,7 +240,7 @@ mod tests { let params = json!({ "digits": 6 }); let err = derive_factor.factor_type.include_params(params); assert!( - matches!(err, Err(crate::error::MFKDF2Error::SerializeError(e)) if e.to_string() == "missing field `hash`") + matches!(err, Err(crate::error::MFKDF2Error::Serialize(e)) if e.to_string() == "missing field `hash`") ); } } diff --git a/mfkdf2/src/derive/factors/mod.rs b/mfkdf2/src/derive/factors/mod.rs index fa09a58c..adddf2a6 100644 --- a/mfkdf2/src/derive/factors/mod.rs +++ b/mfkdf2/src/derive/factors/mod.rs @@ -1,13 +1,29 @@ -pub mod hmacsha1; -pub mod hotp; -pub mod ooba; -pub mod passkey; -pub mod password; +//! Factor construction derive phase +//! +//! This module constructs [`MFKDF2Factor`](`crate::definitions::MFKDF2Factor`) witnesses Wᵢⱼ for +//! the derive phase corresponding to the setup factors defined in [`mod@crate::setup::factors`]. +//! Each helper takes respective factor secret (such as a password, OTP code, UUID, or passkey +//! secret) plus any derive-specific options and constructs a +//! [`MFKDF2Factor`](`crate::definitions::MFKDF2Factor`) that is used in `KeyDerive` derivation. +//! +//! During the [`KeyDerive`](`crate::derive::key::key`) phase, these factors combine with the public +//! policy state βᵢ to reconstruct the underlying static source material κⱼ and ultimately recover +//! the master secret `M` and next derived key state βᵢ₊₁. +//! +//! **Note:** Factor setup/derive individually are not intended to be used in isolation, but are +//! composed through [`setup::key`](`crate::setup::key`) (Setup) and +//! [`derive::key`](`crate::derive::key::key`) (Derive), respectively, where factors supply witness +//! material for the overall multi‑factor policy. +mod hmacsha1; +mod hotp; +mod ooba; +mod passkey; +mod password; pub mod persisted; -pub mod question; -pub mod stack; +mod question; +mod stack; pub mod totp; -pub mod uuid; +mod uuid; pub use hmacsha1::hmacsha1; pub use hotp::hotp; diff --git a/mfkdf2/src/derive/factors/ooba.rs b/mfkdf2/src/derive/factors/ooba.rs index a0eaf839..285b6337 100644 --- a/mfkdf2/src/derive/factors/ooba.rs +++ b/mfkdf2/src/derive/factors/ooba.rs @@ -1,3 +1,9 @@ +//! This module implements the factor construction derive phase for the OOBA construction from +//! [`OOBA`](`crate::setup::factors::ooba()`). +//! - During setup, the factor samples a random 32‑byte target, encrypts it under a channel‑specific +//! RSA key, and embeds an initial code and metadata in the policy. +//! - During derive, this module consumes a user‑entered OOBA code Wᵢⱼ, decrypts the target using +//! the stored pad, and prepares the next encrypted payload and code for the following login use base64::{Engine, engine::general_purpose}; use rsa::Oaep; use serde_json::{Value, json}; @@ -15,6 +21,8 @@ impl FactorDerive for Ooba { type Output = Value; type Params = Value; + /// 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<()> { let pad_b64 = params["pad"].as_str().ok_or(MFKDF2Error::MissingDeriveParams("pad".to_string()))?; @@ -42,6 +50,7 @@ impl FactorDerive for Ooba { Ok(()) } + /// Generates a new OOBA code and encrypts the secret material for the next derivation. fn params(&self, _key: Key) -> MFKDF2Result { let code = generate_alphanumeric_characters(self.length.into()).to_uppercase(); @@ -67,6 +76,81 @@ impl FactorDerive for Ooba { } } +/// Factor construction derive phase for an OOBA factor +/// +/// The `code` should be the alphanumeric value delivered over the out‑of‑band channel (for example, +/// SMS or push notification) that corresponds to the initial OOBA policy parameters created during +/// setup. +/// +/// # Errors +/// +/// - [`MFKDF2Error::InvalidOobaCode`] if `code` is empty +/// - [`MFKDF2Error::MissingDeriveParams`] when required fields such as "pad" or "length" are absent +/// in the policy parameters +/// - [`MFKDF2Error::InvalidDeriveParams`] when fields such as "pad", "params", or "key" are +/// malformed or have the wrong type +/// +/// # Example +/// +/// Single‑factor setup/derive using OOBA within `KeySetup`/`KeyDerive`: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use jsonwebtoken::jwk::Jwk; +/// # use rsa::{RsaPrivateKey, RsaPublicKey, traits::PublicKeyParts}; +/// # use serde_json::json; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::ooba::{OobaOptions}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # }; +/// # use base64::Engine; +/// let bits = 2048; +/// let private_key = +/// RsaPrivateKey::new(&mut rsa::rand_core::OsRng, bits).expect("failed to generate a key"); +/// let public_key = RsaPublicKey::from(&private_key); +/// +/// let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be()); +/// let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be()); +/// let jwk: Jwk = serde_json::from_value(json!({ +/// "kty": "RSA", +/// "alg": "RSA-OAEP-256", +/// "n": n, +/// "e": e +/// }))?; +/// +/// let setup_factor = setup::factors::ooba(OobaOptions { +/// id: Some("ooba".into()), +/// length: Some(8), +/// key: Some(jwk), +/// params: Some(json!({"foo": "bar"})), +/// })?; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// // Decrypt the first OOBA payload to recover the user-visible code +/// let policy_factor = +/// setup_key.policy.factors.iter().find(|f| f.id == "ooba").unwrap(); +/// let setup_params = &policy_factor.params; +/// let ciphertext = hex::decode(setup_params["next"].as_str().unwrap()).unwrap(); +/// let plaintext = private_key.decrypt(rsa::Oaep::new::(), &ciphertext).unwrap(); +/// let decoded: serde_json::Value = serde_json::from_slice(&plaintext).unwrap(); +/// let code = decoded["code"].as_str().unwrap(); +/// +/// let derive_factor = derive::factors::ooba(code)?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("ooba".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn ooba(code: &str) -> MFKDF2Result { if code.is_empty() { return Err(MFKDF2Error::InvalidOobaCode); @@ -85,8 +169,9 @@ pub fn ooba(code: &str) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_ooba(code: &str) -> MFKDF2Result { ooba(code) } +async fn derive_ooba(code: &str) -> MFKDF2Result { ooba(code) } #[cfg(test)] mod tests { @@ -125,7 +210,7 @@ mod tests { params: Some(json!({"foo":"bar"})), }; - let result = crate::setup::factors::ooba::ooba(options); + let result = crate::setup::factors::ooba(options); assert!(result.is_ok()); result.unwrap() @@ -237,7 +322,7 @@ mod tests { key: Some(jwk), params: Some(json!({"foo":"bar"})), }; - let setup = crate::setup::factors::ooba::ooba(options).unwrap(); + let setup = crate::setup::factors::ooba(options).unwrap(); // Setup for derive let setup_params = setup.factor_type.setup().params([0u8; 32].into()).unwrap(); diff --git a/mfkdf2/src/derive/factors/passkey.rs b/mfkdf2/src/derive/factors/passkey.rs index ed7421a4..cd5b1274 100644 --- a/mfkdf2/src/derive/factors/passkey.rs +++ b/mfkdf2/src/derive/factors/passkey.rs @@ -1,9 +1,13 @@ +//! Derive phase [`Passkey`](`crate::setup::factors::passkey()`) construction. It accepts the same +//! 32‑byte secret produced by a `WebAuthn` PRF extension or equivalent hardware‑backed primitive +//! and wraps it as an [`MFKDF2Factor`] used during the derive phase so that the passkey contributes +//! stable 256‑bit entropy across `KeySetup` and `KeyDerive` use serde_json::Value; use crate::{ definitions::{FactorType, MFKDF2Factor}, derive::FactorDerive, - error::{MFKDF2Error, MFKDF2Result}, + error::MFKDF2Result, setup::factors::passkey::Passkey, }; @@ -17,6 +21,50 @@ impl FactorDerive for Passkey { } } +/// Factor construction derive phase for a passkey factor +/// +/// Takes the same 32‑byte secret that was stored at setup time and wraps it in an +/// [`MFKDF2Factor`] suitable for [`crate::derive::key()`]. The factor uses a fixed id `"passkey"` +/// during the derive phase. +/// +/// # Errors +/// +/// - [`MFKDF2Error::InvalidSecretLength`](`crate::error::MFKDF2Error::InvalidSecretLength`) from +/// the bindings helper when a non‑32‑byte slice is provided +/// +/// # Example +/// +/// Single‑factor setup/derive using a passkey within `KeySetup`/`KeyDerive`: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use rand::{RngCore, rngs::OsRng}; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::passkey::{PasskeyOptions}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # }; +/// let mut prf = [0u8; 32]; +/// OsRng.fill_bytes(&mut prf); +/// +/// let setup_factor = setup::factors::passkey(prf, PasskeyOptions::default())?; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// let derive_factor = derive::factors::passkey(prf)?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("passkey".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn passkey(secret: [u8; 32]) -> MFKDF2Result { Ok(MFKDF2Factor { id: Some("passkey".to_string()), @@ -25,10 +73,11 @@ pub fn passkey(secret: [u8; 32]) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_passkey(secret: Vec) -> MFKDF2Result { +async fn derive_passkey(secret: Vec) -> MFKDF2Result { if secret.len() != 32 { - return Err(MFKDF2Error::InvalidSecretLength("passkey".to_string())); + return Err(crate::error::MFKDF2Error::InvalidSecretLength("passkey".to_string())); } passkey(secret.try_into().unwrap()) diff --git a/mfkdf2/src/derive/factors/password.rs b/mfkdf2/src/derive/factors/password.rs index cd3633af..83328ceb 100644 --- a/mfkdf2/src/derive/factors/password.rs +++ b/mfkdf2/src/derive/factors/password.rs @@ -1,3 +1,5 @@ +//! Derive phase [Password](`crate::setup::factors::password()`) construction. Takes a +//! user‑supplied password answer and computes an [`MFKDF2Factor`] witness Wᵢⱼ. use serde_json::{Value, json}; use zxcvbn::zxcvbn; @@ -17,6 +19,47 @@ impl FactorDerive for Password { fn output(&self) -> Self::Output { json!({"strength": zxcvbn(&self.password, &[])}) } } +/// Factor construction derive phase +/// +/// Derives a password factor from a string. Validates the password and returns an [`MFKDF2Factor`] +/// suitable for use with [`crate::derive::key()`]. Unlike setup, the factor constructed for the +/// derive phase does not assign an id or entropy estimate. Those are recovered from the policy +/// during derivation +/// +/// # Errors +/// +/// - [`MFKDF2Error::PasswordEmpty`] if `password` is empty +/// +/// # Example +/// +/// Single‑factor setup/derive using the password factor within `KeySetup`/`KeyDerive`: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::password::{PasswordOptions, password as setup_password}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # derive::factors::password as derive_password, +/// # }; +/// let setup_factor = setup_password("correct horse battery staple", PasswordOptions::default())?; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// let derive_factor = derive_password("correct horse battery staple")?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("password".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn password(password: impl Into) -> MFKDF2Result { let password = std::convert::Into::::into(password); if password.is_empty() { @@ -30,8 +73,9 @@ pub fn password(password: impl Into) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_password(password: String) -> MFKDF2Result { +async fn derive_password(password: String) -> MFKDF2Result { crate::derive::factors::password::password(password) } diff --git a/mfkdf2/src/derive/factors/persisted.rs b/mfkdf2/src/derive/factors/persisted.rs index 399acd72..bb8a04da 100644 --- a/mfkdf2/src/derive/factors/persisted.rs +++ b/mfkdf2/src/derive/factors/persisted.rs @@ -1,15 +1,20 @@ +//! Persisted‑share factor derive. Persistence allows you to save one or more of the factors used to +//! setup a multi-factor derived key (eg. as browser cookies) so that they do not need to be used to +//! derive the key in the future. use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::{ - definitions::{FactorMetadata, FactorType, MFKDF2Factor}, + definitions::{FactorType, MFKDF2Factor, factor::FactorMetadata}, derive::FactorDerive, error::MFKDF2Result, }; +/// Persisted share factor state. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Persisted { + /// Base-64 encoded Shamir share to recover the master secret. pub share: Vec, } @@ -28,6 +33,39 @@ impl FactorDerive for Persisted { fn output(&self) -> Self::Output { Value::Null } } +/// Factor construction derive phase for a persisted Shamir share +/// +/// The `share` should be the byte slice previously obtained from a derived key via +/// [`MFKDF2DerivedKey::persist_factor()`](`crate::definitions::MFKDF2DerivedKey::persist_factor()`). This factor constructs a [`Persisted`] factor that can be +/// passed directly to [`crate::derive::key()`] without requiring any additional user interaction. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # derive, +/// # setup::{ +/// # self, +/// # factors::password::{PasswordOptions, password as setup_password}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::persisted::persisted, +/// # }; +/// # +/// let setup_key = setup::key( +/// &[setup_password("password", PasswordOptions::default())?], +/// MFKDF2Options::default(), +/// )?; +/// let share = setup_key.persist_factor("password"); +/// let factor = persisted(share)?; +/// +/// let derived = +/// derive::key(&setup_key.policy, HashMap::from([("password".to_string(), factor)]), true, false)?; +/// assert_eq!(derived.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn persisted(share: Vec) -> MFKDF2Result { Ok(MFKDF2Factor { id: Some("persisted".to_string()), @@ -36,8 +74,9 @@ pub fn persisted(share: Vec) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_persisted(share: Vec) -> MFKDF2Result { persisted(share) } +async fn derive_persisted(share: Vec) -> MFKDF2Result { persisted(share) } #[cfg(test)] mod tests { diff --git a/mfkdf2/src/derive/factors/question.rs b/mfkdf2/src/derive/factors/question.rs index d1fa325e..25c37c80 100644 --- a/mfkdf2/src/derive/factors/question.rs +++ b/mfkdf2/src/derive/factors/question.rs @@ -1,3 +1,7 @@ +//! Derive phase [Question](`crate::setup::factors::question()`) construction. It accepts a raw user +//! answer, normalizes it, and returns an [`MFKDF2Factor`] used in the derive phase. The factor also +//! exposes a strength estimate via `output()` so callers can compare entropy between setup and +//! derive use serde_json::{Value, json}; use zxcvbn::zxcvbn; @@ -19,9 +23,54 @@ impl FactorDerive for Question { fn params(&self, _key: Key) -> MFKDF2Result { Ok(self.params.clone()) } + /// Returns a strength estimate for the answer using `zxcvbn`. fn output(&self) -> Self::Output { json!({"strength": zxcvbn(&self.answer, &[])}) } } +/// Factor construction derive phase for a security‑question factor +/// +/// The answer is normalized (lower‑cased, punctuation removed, and surrounding whitespace trimmed) +/// to match the behaviour of the setup‑time [question](`crate::setup::factors::question::question`) +/// helper. The resulting [`MFKDF2Factor`] has no id or entropy assigned during the derive phase; +/// those are pulled from the policy when combining factors with [`crate::derive::key()`] +/// +/// # Errors +/// +/// - [`MFKDF2Error::AnswerEmpty`] if the provided answer is empty +/// +/// # Example +/// +/// Single‑factor setup/derive using a security‑question factor within KeySetup/KeyDerive: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::question::{QuestionOptions, question as setup_question}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # }; +/// # use mfkdf2::derive::factors::question as derive_question; +/// let setup_factor = setup_question("Blue! Is My Favorite Color.", QuestionOptions { +/// id: Some("question".into()), +/// question: Some("prompt".into()), +/// })?; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// let derive_factor = derive_question(" Blue! Is My Favorite Color. ")?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("question".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn question(answer: impl Into) -> MFKDF2Result { let answer = answer.into(); if answer.is_empty() { @@ -46,8 +95,9 @@ pub fn question(answer: impl Into) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_question(answer: String) -> MFKDF2Result { question(answer) } +async fn derive_question(answer: String) -> MFKDF2Result { question(answer) } #[cfg(test)] mod tests { diff --git a/mfkdf2/src/derive/factors/stack.rs b/mfkdf2/src/derive/factors/stack.rs index b4f3f09a..86887bbe 100644 --- a/mfkdf2/src/derive/factors/stack.rs +++ b/mfkdf2/src/derive/factors/stack.rs @@ -1,3 +1,10 @@ +//! Stack factor derive +//! +//! This module implements the factor construction derive phase for the stack factor from +//! [`Stack`](`crate::setup::factors::stack()`). A stack factor treats an entire derived key (built +//! from one or more underlying factors) as a single higher‑level factor. During derive it accepts a +//! map of inner witnesses Wᵢⱼ, reconstructs the stacked key using [`crate::derive::key::key`] in +//! stack mode, and exposes the resulting policy and key material as a single factor use std::collections::HashMap; use serde_json::{Value, json}; @@ -31,6 +38,58 @@ impl FactorDerive for Stack { fn output(&self) -> Self::Output { serde_json::to_value(&self.key).unwrap_or(json!({})) } } +/// Factor construction derive phase for a stack factor +/// +/// The `factors` map should contain witnesses used in the derive phase for the inner factors that +/// were used to construct the stacked key during setup, keyed by their factor ids. This helper +/// wraps them in a `Stack` factor that, when passed to [`KeyDerive`](`crate::derive::key::key`) +/// along with the appropriate policy, reconstructs the stacked key in stack mode. +/// +/// # Errors +/// +/// - [`MFKDF2Error::InvalidDeriveParams`] with `"factors"` when `factors` is empty +/// - [`MFKDF2Error::InvalidDeriveParams`] when the provided policy JSON cannot be deserialized into +/// a [`crate::policy::Policy`] +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::{ +/// # password::{PasswordOptions, password as setup_password}, +/// # stack::{StackOptions, stack as setup_stack}, +/// # }, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::{password as derive_password, stack as derive_stack}, +/// # }; +/// let f1 = setup_password("password123", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = setup_password("password456", PasswordOptions { id: Some("pwd2".into()) })?; +/// let stack_factor = setup_stack(vec![f1, f2], StackOptions { +/// id: Some("my-stack".into()), +/// threshold: Some(2), +/// salt: None, +/// })?; +/// let setup_key = setup::key(&[stack_factor], MFKDF2Options::default())?; +/// +/// let mut inner = HashMap::new(); +/// inner.insert("pwd1".to_string(), derive_password("password123")?); +/// inner.insert("pwd2".to_string(), derive_password("password456")?); +/// let derive_stack_factor = derive_stack(inner)?; +/// +/// let derived_key = mfkdf2::derive::key( +/// &setup_key.policy, +/// HashMap::from([("my-stack".to_string(), derive_stack_factor)]), +/// false, +/// false, +/// )?; +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn stack(factors: HashMap) -> MFKDF2Result { if factors.is_empty() { return Err(MFKDF2Error::InvalidDeriveParams("factors".to_string())); @@ -43,8 +102,9 @@ pub fn stack(factors: HashMap) -> MFKDF2Result) -> MFKDF2Result { +async fn derive_stack(factors: HashMap) -> MFKDF2Result { stack(factors) } @@ -54,13 +114,11 @@ mod tests { use super::*; use crate::{ + definitions::MFKDF2Options, derive::factors::password::password, - setup::{ - factors::{ - password::{PasswordOptions, password as setup_password}, - stack::{StackOptions, stack as setup_stack}, - }, - key::MFKDF2Options, + setup::factors::{ + password::{PasswordOptions, password as setup_password}, + stack::{StackOptions, stack as setup_stack}, }, }; @@ -75,7 +133,6 @@ mod tests { StackOptions { id: Some("my-stack".to_string()), threshold: Some(2), salt: None }; let stack_factor = setup_stack(factors, options).unwrap(); - // let params = stack_factor.factor_type.params_setup([0; 32]); crate::setup::key(&[stack_factor], MFKDF2Options::default()) .expect("derived key should be created") diff --git a/mfkdf2/src/derive/factors/totp.rs b/mfkdf2/src/derive/factors/totp.rs index f9213558..ff6c1269 100644 --- a/mfkdf2/src/derive/factors/totp.rs +++ b/mfkdf2/src/derive/factors/totp.rs @@ -1,3 +1,10 @@ +//! Factor construction derive phase for the TOTP factor from +//! [TOTP](`mod@crate::setup::factors::totp`). +//! +//! - During setup, the factor precomputes a window of offsets and stores them along with an +//! encrypted TOTP secret in the policy. +//! - During derive, this module consumes a time‑based TOTP code Wᵢⱼ and reconstructs the same +//! target code σₜ within the configured time window, refreshing offsets for future logins use std::collections::HashMap; #[cfg(not(target_arch = "wasm32"))] use std::time::{SystemTime, UNIX_EPOCH}; @@ -13,17 +20,20 @@ use crate::{ definitions::{FactorType, Key, MFKDF2Factor}, derive::FactorDerive, error::{MFKDF2Error, MFKDF2Result}, - otpauth::generate_hotp_code, + otpauth::generate_otp_token, setup::factors::{ hotp::mod_positive, totp::{TOTP, TOTPConfig, TOTPParams}, }, }; +/// Options for configuring a TOTP factor derive. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct TOTPDeriveOptions { + /// Unix time in milliseconds used for derive; defaults to the current system time when omitted pub time: Option, + /// Optional timing oracle to harden TOTP factor construction pub oracle: Option>, } @@ -43,6 +53,8 @@ impl FactorDerive for TOTP { type Output = Value; type Params = Value; + /// Stores the public parameters for the TOTP factor. + /// Calculates the offset index from start time and current time, and derives the target code. fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()> { self.params = params.clone(); @@ -85,7 +97,7 @@ impl FactorDerive for TOTP { Ok(()) } - /// Note: `self.options` is only used for [`TOTPDeriveOptions`]. + /// Decrypts the secret and generates the new codes in the time window. fn params(&self, key: Key) -> MFKDF2Result { let params: TOTPParams = serde_json::from_value(self.params.clone())?; @@ -97,7 +109,7 @@ impl FactorDerive for TOTP { for i in 0..params.window { let counter = (time / 1000) / (params.step as u64) + i as u64; - let code = generate_hotp_code(&padded_secret[..20], counter, ¶ms.hash, params.digits); + let code = generate_otp_token(&padded_secret[..20], counter, ¶ms.hash, params.digits); let mut offset = mod_positive(i64::from(self.target) - i64::from(code), 10_i64.pow(params.digits)); @@ -130,6 +142,71 @@ impl FactorDerive for TOTP { } } +/// Factor construction derive phase for a TOTP factor +/// +/// The `code` should be the numeric TOTP value displayed by a standard authenticator app that was +/// paired with the secret configured during setup. `options` can override the effective time and +/// oracle behaviour for advanced flows; by default, the current system time is used and no oracle +/// adjustments are applied. +/// +/// # Errors +/// +/// - [`MFKDF2Error::MissingDeriveParams`] if required fields such as "time" are missing when +/// converting [`TOTPDeriveOptions`] into [`TOTPConfig`] (this is avoided when `options` is `None` +/// and the default time is used) +/// - [`MFKDF2Error::TOTPWindowExceeded`] when the effective time lies outside the precomputed +/// window encoded in the policy +/// - [`MFKDF2Error::InvalidDeriveParams`] when the offsets buffer is malformed or too small for the +/// computed index +/// +/// # Example +/// +/// Single‑factor setup/derive using TOTP within KeySetup/KeyDerive: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use std::time::{SystemTime, UNIX_EPOCH}; +/// use mfkdf2::{ +/// derive, +/// derive::factors::totp::{TOTPDeriveOptions, totp}, +/// error::MFKDF2Result, +/// otpauth::HashAlgorithm, +/// setup::{ +/// self, +/// factors::totp::{TOTPOptions, totp as setup_totp}, +/// }, +/// definitions::MFKDF2Options, +/// }; +/// let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; +/// let options = +/// TOTPOptions { secret: Some(b"hello world mfkdf2!!".to_vec()), ..Default::default() }; +/// +/// let setup_factor = setup_totp(options)?; +/// # let secret = if let mfkdf2::definitions::FactorType::TOTP(ref f) = setup_factor.factor_type { +/// # f.config.secret.clone() +/// # } else { +/// # unreachable!() +/// # }; +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// # let step = 30; +/// # let digits = 6; +/// # let hash = HashAlgorithm::Sha1; +/// # let counter = now_ms / (step * 1000); +/// # let code = mfkdf2::otpauth::generate_otp_token(&secret[..20], counter, &hash, digits); +/// +/// let derive_options = TOTPDeriveOptions { time: Some(now_ms), oracle: None }; +/// let derive_factor = totp(code, Some(derive_options))?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("totp".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn totp(code: u32, options: Option) -> MFKDF2Result { let mut options = options.unwrap_or_default(); @@ -151,11 +228,9 @@ pub fn totp(code: u32, options: Option) -> MFKDF2Result, -) -> MFKDF2Result { +async fn derive_totp(code: u32, options: Option) -> MFKDF2Result { totp(code, options) } @@ -229,7 +304,7 @@ mod tests { let now_millis = time; let counter = now_millis / (u64::from(step) * 1000); - let correct_code = generate_hotp_code(&secret[..20], counter, &hash, digits); + let correct_code = generate_otp_token(&secret[..20], counter, &hash, digits); let derive_options = get_test_derive_totp_options(Some(time)); let mut derive_material = totp(correct_code, Some(derive_options)).unwrap(); diff --git a/mfkdf2/src/derive/factors/uuid.rs b/mfkdf2/src/derive/factors/uuid.rs index 3ad0ca98..eea273cb 100644 --- a/mfkdf2/src/derive/factors/uuid.rs +++ b/mfkdf2/src/derive/factors/uuid.rs @@ -1,3 +1,9 @@ +//! UUID factor derive +//! +//! Derive phase [UUID](`crate::setup::factors::uuid()`) construction. It turns a stable +//! UUID value into an [`MFKDF2Factor`] used during the derive phase. It is typically used in flows +//! where a device, account, or hardware identifier is known at both setup and derive and acts as a +//! non‑interactive high‑entropy factor use serde_json::json; use uuid::Uuid; @@ -21,6 +27,40 @@ impl FactorDerive for UUIDFactor { } } +/// Factor construction derive phase for a UUID factor +/// +/// # Example +/// +/// Single‑factor setup/derive using a UUID factor within KeySetup/KeyDerive: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use uuid::Uuid; +/// # use mfkdf2::{ +/// # error::MFKDF2Result, +/// # setup::{ +/// # self, +/// # factors::uuid::{uuid as setup_uuid, UUIDOptions}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive, +/// # derive::factors::uuid as derive_uuid, +/// # }; +/// let id = Uuid::parse_str("f9bf78b9-54e7-4696-97dc-5e750de4c592").unwrap(); +/// let setup_factor = setup_uuid(UUIDOptions { id: Some("uuid".into()), uuid: Some(id) }).unwrap(); +/// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?; +/// +/// let derive_factor = derive_uuid(id)?; +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("uuid".to_string(), derive_factor)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn uuid(uuid: Uuid) -> MFKDF2Result { Ok(MFKDF2Factor { id: None, @@ -29,8 +69,9 @@ pub fn uuid(uuid: Uuid) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn derive_uuid(uuid: Uuid) -> MFKDF2Result { +async fn derive_uuid(uuid: Uuid) -> MFKDF2Result { crate::derive::factors::uuid(uuid) } diff --git a/mfkdf2/src/derive/key.rs b/mfkdf2/src/derive/key.rs index 664a82e5..b97c8778 100644 --- a/mfkdf2/src/derive/key.rs +++ b/mfkdf2/src/derive/key.rs @@ -1,3 +1,6 @@ +//! The core MFKDF2 algorithm serves as a foundational primitive for deriving a high-entropy +//! master key from a multi-factor policy. Key Derive phase takes a derived policy state βᵢ and +//! factor witnesses Wᵢⱼ, and reconstructs the master secret M, along with the next state βᵢ₊₁. use std::{collections::HashMap, fmt::Write}; use argon2::{Argon2, Params, Version}; @@ -13,13 +16,264 @@ use crate::{ policy::Policy, }; +/// Performs `KeyDerive` on an existing policy and a set of derive‑time factor witnesses +/// +/// This function implements the derive phase described in [`crate::derive`], taking a policy state +/// βᵢ and factor witnesses Wᵢⱼ, reconstructing the master secret M, regenerating the key‑encryption +/// key (KEK), decrypting the current key Kᵢ, and producing a fresh [`MFKDF2DerivedKey`] with +/// updated factor parameters and integrity metadata +/// +/// # Arguments +/// +/// * `policy`: [`Policy`] βᵢ produced during `KeySetup` that encodes threshold, helper data, and +/// encrypted Shamir shares +/// * `factors`: Derived [`MFKDF2Factor`] witnesses Wᵢⱼ +/// * `verify`: Policy verification flag to check the stored policy HMAC against the derived key +/// material and returns an error when the integrity check fails +/// * `stack`: Enables stack‑based factor derivation +/// +/// # Returns +/// +/// On success, returns an [`MFKDF2DerivedKey`] representing Kᵢ₊₁ with: +/// +/// * A possibly updated [`Policy`] reflecting refreshed factor parameters and integrity HMAC +/// * A 32‑byte key `K` +/// * Recovered `secret` and Shamir shares consistent with the provided witnesses +/// * Per‑factor outputs produced by the factor derive algorithms +/// +/// # Examples +/// +/// Password and HMAC‑SHA1 round‑trip where `derive` reconstructs the same key and secret as setup +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # setup::{ +/// # self, +/// # factors::{ +/// # hmacsha1::{HmacSha1Options, hmacsha1 as setup_hmacsha1}, +/// # password::{PasswordOptions, password as setup_password}, +/// # }, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::{ +/// # self, +/// # factors::{ +/// # password as derive_password, +/// # hmacsha1 as derive_hmacsha1, +/// # }, +/// # }, +/// # }; +/// # use hmac::{Mac, Hmac}; +/// # use sha1::Sha1; +/// # const HMACSHA1_SECRET: [u8; 20] = [0u8; 20]; +/// let password_factor = +/// setup_password("password123", PasswordOptions { id: Some("pwd".to_string()) })?; +/// let hmac_factor = setup_hmacsha1(HmacSha1Options { +/// secret: Some(HMACSHA1_SECRET.to_vec()), +/// ..Default::default() +/// })?; +/// +/// let setup_key = setup::key(&[password_factor, hmac_factor.clone()], MFKDF2Options::default())?; +/// +/// // Build derive‑time password witness +/// let derive_pwd = derive_password("password123")?; +/// +/// // Build derive‑time HMAC witness using the challenge from policy +/// let policy_hmac = setup_key.policy.factors.iter().find(|f| f.id == "hmacsha1").unwrap(); +/// let challenge = hex::decode(policy_hmac.params["challenge"].as_str().unwrap()).unwrap(); +/// let secret = if let mfkdf2::definitions::FactorType::HmacSha1(h) = &hmac_factor.factor_type { +/// &h.padded_secret[..20] +/// } else { +/// unreachable!() +/// }; +/// let response: [u8; 20] = as Mac>::new_from_slice(&HMACSHA1_SECRET) +/// .unwrap() +/// .chain_update(challenge) +/// .finalize() +/// .into_bytes() +/// .into(); +/// let derive_hmac = derive_hmacsha1(response.into())?; +/// +/// let derived_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("pwd".to_string(), derive_pwd), ("hmacsha1".to_string(), derive_hmac)]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// assert_eq!(derived_key.secret, setup_key.secret); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Threshold derivation with password, HOTP, and TOTP where only a 2‑of‑3 subset is supplied +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use std::time::{SystemTime, UNIX_EPOCH}; +/// # use mfkdf2::{ +/// # otpauth::generate_otp_token, +/// # setup::{ +/// # self, +/// # factors::{ +/// # hotp::{HOTPOptions, hotp as setup_hotp}, +/// # password::{PasswordOptions, password as setup_password}, +/// # totp::{TOTPOptions, totp as setup_totp}, +/// # }, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::{password as derive_password, hotp as derive_hotp}, +/// # derive, +/// # }; +/// let setup_pwd = setup_password("password123", PasswordOptions::default())?; +/// let setup_hotp = setup_hotp(HOTPOptions::default())?; +/// let setup_totp = setup_totp(TOTPOptions::default())?; +/// +/// let options = MFKDF2Options { threshold: Some(2), ..MFKDF2Options::default() }; +/// let setup_key = +/// setup::key(&[setup_pwd.clone(), setup_hotp.clone(), setup_totp.clone()], options)?; +/// +/// let mut factors = HashMap::new(); +/// +/// // Password witness +/// let derive_pwd = derive_password("password123")?; +/// factors.insert("password".to_string(), derive_pwd); +/// +/// // HOTP witness +/// let policy_hotp = setup_key.policy.factors.iter().find(|f| f.id == "hotp").unwrap(); +/// let hotp_params = &policy_hotp.params; +/// let counter = hotp_params["counter"].as_u64().unwrap(); +/// let hotp = match setup_hotp.factor_type { +/// mfkdf2::definitions::FactorType::HOTP(ref h) => h, +/// _ => unreachable!(), +/// }; +/// let hotp_code = +/// generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); +/// let derive_hotp = derive_hotp(hotp_code as u32)?; +/// factors.insert("hotp".to_string(), derive_hotp); +/// +/// // Only 2 of the 3 factors are supplied +/// let derived_key = derive::key(&setup_key.policy, factors, false, false)?; +/// +/// assert_eq!(derived_key.key, setup_key.key); +/// assert_eq!(derived_key.secret, setup_key.secret); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Persisted factor example where a single HOTP factor is persisted during setup and later used +/// directly as a witness during derive alongside a password factor +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # derive, +/// # setup::{ +/// # self, +/// # factors::{ +/// # hotp::HOTPOptions, +/// # password::{PasswordOptions, password as setup_password}, +/// # }, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::{persisted as derive_persisted, password as derive_password}, +/// # }; +/// let setup_factors = &[ +/// setup::factors::hotp::hotp(HOTPOptions::default())?, +/// setup_password("password", PasswordOptions::default())?, +/// ]; +/// let setup_key = setup::key(setup_factors, MFKDF2Options::default())?; +/// +/// let persisted = setup_key.persist_factor("hotp"); +/// let derived = derive::key( +/// &setup_key.policy, +/// HashMap::from([ +/// ("hotp".to_string(), derive_persisted(persisted)?), +/// ("password".to_string(), derive_password("password")?), +/// ]), +/// true, +/// false, +/// )?; +/// +/// assert_eq!(derived.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// # Errors +/// +/// The function returns invalid key when the provided witnesses do not reconstruct a consistent set +/// of Shamir shares, for example when an OTP factor is incorrect +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # setup::{ +/// # self, +/// # factors::hotp::{HOTPOptions, hotp as setup_hotp}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::hotp as derive_hotp, +/// # derive, +/// # }; +/// let setup_key = setup::key(&[setup_hotp(HOTPOptions::default())?], MFKDF2Options::default())?; +/// +/// // Deliberately wrong HOTP code +/// let wrong_hotp = derive_hotp(123456)?; +/// +/// let derive_key = derive::key( +/// &setup_key.policy, +/// HashMap::from([("hotp".to_string(), wrong_hotp)]), +/// false, +/// false, +/// )?; +/// +/// assert_ne!(derive_key.key, setup_key.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// The function returns `Err(MFKDF2Error::PolicyIntegrityCheckFailed)` when `verify` is `true` and +/// the stored policy HMAC does not match the recomputed integrity digest, for example when the +/// policy has been tampered with between setup and derive +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::{ +/// # setup::{ +/// # self, +/// # factors::password::{PasswordOptions, password as setup_password}, +/// # }, +/// # definitions::MFKDF2Options, +/// # derive::factors::password as derive_password, +/// # derive, +/// # }; +/// let setup_key = setup::key( +/// &[setup_password("password123", PasswordOptions { id: Some("password".to_string()) })?], +/// MFKDF2Options::default(), +/// )?; +/// +/// let mut corrupted_policy = setup_key.policy.clone(); +/// corrupted_policy.hmac = "corrupted".to_string(); +/// +/// let derive_factor = derive_password("password123")?; +/// let result = derive::key( +/// &corrupted_policy, +/// HashMap::from([("password".to_string(), derive_factor)]), +/// true, +/// false, +/// ); +/// +/// assert!(matches!(result, Err(mfkdf2::error::MFKDF2Error::PolicyIntegrityCheckFailed))); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn key( policy: &Policy, factors: HashMap, verify: bool, stack: bool, ) -> MFKDF2Result { - assert!(factors.len() < 256, "MFKDF2 supports at most 255 factors"); + if factors.len() > 255 { + return Err(MFKDF2Error::TooManyFactors); + } let mut shares_bytes = Vec::new(); let mut outputs = HashMap::new(); @@ -87,15 +341,13 @@ pub fn key( let shares_vec: Vec>> = shares_bytes .into_iter() .map(|opt| { - opt - .map(|b| Share::try_from(b.as_slice()).map_err(|_| MFKDF2Error::TryFromVecError)) - .transpose() + opt.map(|b| Share::try_from(b.as_slice()).map_err(|_| MFKDF2Error::TryFromVec)).transpose() }) .collect::>>, _>>()?; let sss = SecretSharing(policy.threshold); - let secret = sss.recover(&shares_vec).map_err(|_| MFKDF2Error::ShareRecoveryError)?; - let secret_arr: [u8; 32] = secret[..32].try_into().map_err(|_| MFKDF2Error::TryFromVecError)?; + let secret = sss.recover(&shares_vec).map_err(|_| MFKDF2Error::ShareRecovery)?; + let secret_arr: [u8; 32] = secret[..32].try_into().map_err(|_| MFKDF2Error::TryFromVec)?; let salt_bytes = general_purpose::STANDARD.decode(&policy.salt)?; // Generate key @@ -157,7 +409,7 @@ pub fn key( shares_vec.iter().map(|s| s.as_ref()).collect::>>>(), policy.factors.len(), ) - .map_err(|_| MFKDF2Error::ShareRecoveryError)?; + .map_err(|_| MFKDF2Error::ShareRecovery)?; Ok(MFKDF2DerivedKey { policy: new_policy, @@ -169,8 +421,9 @@ pub fn key( }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export(default(verify = true, stack = false)))] -pub async fn derive_key( +async fn derive_key( policy: &Policy, factors: HashMap, verify: bool, @@ -194,16 +447,15 @@ mod tests { use super::*; use crate::{ - definitions::FactorType, + definitions::{FactorType, MFKDF2Options}, derive::{ self, factors::{ - hmacsha1::hmacsha1 as derive_hmacsha1, hotp::hotp as derive_hotp, - ooba::ooba as derive_ooba, passkey::passkey as derive_passkey, - password::password as derive_password, persisted, totp::totp as derive_totp, + hmacsha1 as derive_hmacsha1, hotp as derive_hotp, ooba as derive_ooba, + passkey as derive_passkey, password as derive_password, persisted, totp as derive_totp, }, }, - otpauth::generate_hotp_code, + otpauth::generate_otp_token, setup::{ self, factors::{ @@ -214,7 +466,6 @@ mod tests { password::{PasswordOptions, password as setup_password}, totp::{TOTPOptions, totp as setup_totp}, }, - key::MFKDF2Options, }, }; @@ -259,10 +510,11 @@ mod tests { #[test] fn key_derivation_round_trip_password_only() { // Setup phase - let mut setup_factor = setup_password("password123", PasswordOptions::default()).unwrap(); - setup_factor.id = Some("pwd".to_string()); - let setup_derived_key = - setup::key::key(&[setup_factor.clone()], setup::key::MFKDF2Options::default()).unwrap(); + let setup_derived_key = setup::key( + &[setup_password("password123", PasswordOptions { id: Some("pwd".to_string()) }).unwrap()], + MFKDF2Options::default(), + ) + .unwrap(); // Derivation phase let mut derive_factors_map = HashMap::new(); @@ -284,22 +536,18 @@ mod tests { #[test] fn key_derivation_round_trip_password_and_hmac() { // Setup phase - let mut setup_password_factor = - setup_password("password123", PasswordOptions::default()).unwrap(); - setup_password_factor.id = Some("pwd".to_string()); + let setup_password_factor = + setup_password("password123", PasswordOptions { id: Some("pwd".to_string()) }).unwrap(); - let mut setup_hmac_factor = setup_hmacsha1(HmacSha1Options { + let setup_hmac_factor = setup_hmacsha1(HmacSha1Options { id: Some("hmac".to_string()), secret: Some(HMACSHA1_SECRET.to_vec()), }) .unwrap(); - setup_hmac_factor.id = Some("hmac".to_string()); - let setup_derived_key = setup::key::key( - &[setup_password_factor.clone(), setup_hmac_factor.clone()], - setup::key::MFKDF2Options::default(), - ) - .unwrap(); + let setup_derived_key = + setup::key(&[setup_password_factor, setup_hmac_factor.clone()], MFKDF2Options::default()) + .unwrap(); // Derivation phase let mut derive_factors_map = HashMap::new(); @@ -353,9 +601,9 @@ mod tests { let mut setup_ooba_factor = generate_ooba_setup_factor("ooba", &public_key); setup_ooba_factor.id = Some("ooba".to_string()); - let setup_derived_key = setup::key::key( + let setup_derived_key = setup::key( &[setup_hotp_factor.clone(), setup_totp_factor.clone(), setup_ooba_factor.clone()], - setup::key::MFKDF2Options::default(), + MFKDF2Options::default(), ) .unwrap(); @@ -368,7 +616,7 @@ mod tests { let hotp_params = &policy_hotp_factor.params; let counter = hotp_params["counter"].as_u64().unwrap(); let correct_code = - generate_hotp_code(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); + generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); let mut derive_hotp_factor = derive_hotp(correct_code as u32).unwrap(); derive_hotp_factor.id = Some("hotp".to_string()); derive_factors_map.insert("hotp".to_string(), derive_hotp_factor); @@ -377,7 +625,7 @@ mod tests { let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); let counter = time as u64 / (totp.config.step as u64 * 1000); let totp_code = - generate_hotp_code(&totp.config.secret[..20], counter, &totp.config.hash, totp.config.digits); + generate_otp_token(&totp.config.secret[..20], counter, &totp.config.hash, totp.config.digits); let mut derive_totp_factor = derive_totp(totp_code as u32, None).unwrap(); derive_totp_factor.id = Some("totp".to_string()); derive_factors_map.insert("totp".to_string(), derive_totp_factor); @@ -411,7 +659,7 @@ mod tests { let hotp_params = &policy_hotp_factor.params; let counter = hotp_params["counter"].as_u64().unwrap(); let correct_code = - generate_hotp_code(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); + generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); let mut derive_hotp_factor = derive_hotp(correct_code as u32).unwrap(); derive_hotp_factor.id = Some("hotp".to_string()); derive_factors_map.insert("hotp".to_string(), derive_hotp_factor); @@ -420,7 +668,7 @@ mod tests { let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); let counter = time as u64 / (u64::from(totp.config.step) * 1000); let totp_code = - generate_hotp_code(&totp.config.secret[..20], counter, &totp.config.hash, totp.config.digits); + generate_otp_token(&totp.config.secret[..20], counter, &totp.config.hash, totp.config.digits); let mut derive_totp_factor = derive_totp(totp_code as u32, None).unwrap(); derive_totp_factor.id = Some("totp".to_string()); derive_factors_map.insert("totp".to_string(), derive_totp_factor); @@ -462,8 +710,8 @@ mod tests { let mut setup_totp_factor = setup_totp(TOTPOptions::default()).unwrap(); setup_totp_factor.id = Some("totp".to_string()); - let options = setup::key::MFKDF2Options { threshold: Some(2), ..Default::default() }; - let setup_derived_key = setup::key::key( + let options = MFKDF2Options { threshold: Some(2), ..Default::default() }; + let setup_derived_key = setup::key( &[setup_password_factor.clone(), setup_hotp_factor.clone(), setup_totp_factor.clone()], options, ) @@ -483,7 +731,7 @@ mod tests { let hotp_params = &policy_hotp_factor.params; let counter = hotp_params["counter"].as_u64().unwrap(); let correct_code = - generate_hotp_code(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); + generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); let mut derive_hotp_factor = derive_hotp(correct_code as u32).unwrap(); derive_hotp_factor.id = Some("hotp".to_string()); derive_factors_map.insert("hotp".to_string(), derive_hotp_factor); @@ -535,8 +783,8 @@ mod tests { setup_totp_factor.clone(), setup_ooba_factor.clone(), ]; - let options = setup::key::MFKDF2Options { threshold: Some(3), ..Default::default() }; - let setup_derived_key = setup::key::key(setup_factors, options).unwrap(); + let options = MFKDF2Options { threshold: Some(3), ..Default::default() }; + let setup_derived_key = setup::key(setup_factors, options).unwrap(); // Derivation phase let mut derive_factors_map = HashMap::new(); @@ -563,7 +811,7 @@ mod tests { let hotp_params = &policy_hotp_factor.params; let counter = hotp_params["counter"].as_u64().unwrap(); let correct_code = - generate_hotp_code(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); + generate_otp_token(&hotp.config.secret[..20], counter, &hotp.config.hash, hotp.config.digits); let mut derive_hotp_factor = derive_hotp(correct_code as u32).unwrap(); derive_hotp_factor.id = Some("hotp".to_string()); derive_factors_map.insert("hotp".to_string(), derive_hotp_factor); @@ -585,7 +833,7 @@ mod tests { setup_password("password789", PasswordOptions { id: Some("pwd3".to_string()) }).unwrap(), ]; let setup_derived_key = - setup::key::key(setup_factors, MFKDF2Options { threshold: Some(2), ..Default::default() }) + setup::key(setup_factors, MFKDF2Options { threshold: Some(2), ..Default::default() }) .unwrap(); // Derivation phase @@ -631,7 +879,7 @@ mod tests { setup_hotp(HOTPOptions::default())?, setup_password("password", PasswordOptions::default())?, ]; - let setup_derived_key = setup::key::key(setup_factors, MFKDF2Options::default())?; + let setup_derived_key = setup::key(setup_factors, MFKDF2Options::default())?; let hotp = setup_derived_key.persist_factor("hotp"); @@ -654,7 +902,7 @@ mod tests { let mut prf = [0u8; 32]; OsRng.fill_bytes(&mut prf); let setup_derived_key = - setup::key::key(&[setup_passkey(prf, PasskeyOptions::default())?], MFKDF2Options::default())?; + setup::key(&[setup_passkey(prf, PasskeyOptions::default())?], MFKDF2Options::default())?; let derive = derive::key( &setup_derived_key.policy, @@ -672,7 +920,7 @@ mod tests { let mut prf = [0u8; 32]; OsRng.fill_bytes(&mut prf); let setup_derived_key = - setup::key::key(&[setup_passkey(prf, PasskeyOptions::default())?], MFKDF2Options::default())?; + setup::key(&[setup_passkey(prf, PasskeyOptions::default())?], MFKDF2Options::default())?; let mut prf2 = [0u8; 32]; OsRng.fill_bytes(&mut prf2); diff --git a/mfkdf2/src/derive/mod.rs b/mfkdf2/src/derive/mod.rs index 49edb36b..29d1555c 100644 --- a/mfkdf2/src/derive/mod.rs +++ b/mfkdf2/src/derive/mod.rs @@ -1,5 +1,19 @@ +//! # MFKDF2 Key Derivation +//! +//! For i+1-th derivation of [`MFKDF2DerivedKey`](`crate::definitions::MFKDF2DerivedKey`), +//! [`KeyDerive`](`crate::derive::key::key`) takes every factor witnesses Wᵢⱼ and public state +//! βᵢⱼ (from key's inner state) and produces the updated key K and next state βᵢ₊₁ +//! +//! # Factor Derive +//! +//! Derive algorithm for i-th derivation takes a j-th factor's witness Wᵢⱼ and the public +//! parameter βᵢⱼ and outputs the next state βᵢ₊₁,ⱼ and the source key material κⱼ. +//! `KeyDerive` performs this for every factor (up to the threshold). During Derive, the factor's +//! witness W is combined with public helper data to reconstruct the static κ. Thus, σ is the +//! underlying secret that "powers" the factor, while κ is the consistent value that the factor +//! contributes to the final key derivation. pub mod factors; -pub mod key; +mod key; pub use key::key; use serde_json::Value; @@ -9,15 +23,20 @@ use crate::{ error::MFKDF2Result, }; -#[allow(unused_variables)] -pub trait FactorDerive: Send + Sync + std::fmt::Debug { +/// Trait for factor derive. +pub(crate) trait FactorDerive: Send + Sync + std::fmt::Debug { + /// Public parameters for the factor derive. type Params: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Default; + /// Public output for the factor derive. type Output: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Default; + /// Includes the public parameters and witness for the factor derive in factor state fn include_params(&mut self, params: Self::Params) -> MFKDF2Result<()>; - fn params(&self, key: Key) -> MFKDF2Result { + /// Returns the public parameters for the factor derive. + fn params(&self, _key: Key) -> MFKDF2Result { Ok(serde_json::from_value(serde_json::json!({}))?) } + /// Returns the public output for the factor derive. fn output(&self) -> Self::Output { serde_json::from_value(serde_json::json!({})).unwrap() } } @@ -66,14 +85,16 @@ impl FactorDerive for FactorType { fn output(&self) -> Self::Output { self.derive().output() } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derive_factor_params(factor: &FactorType, key: Option) -> MFKDF2Result { +fn derive_factor_params(factor: &FactorType, key: Option) -> MFKDF2Result { let key = key.unwrap_or_else(|| [0u8; 32].into()); factor.params(key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn derive_factor_output(factor: &FactorType) -> Value { factor.output() } +fn derive_factor_output(factor: &FactorType) -> Value { factor.output() } #[cfg(test)] mod tests { @@ -84,7 +105,7 @@ mod tests { use serde_json::json; use crate::{ - definitions::MFKDF2DerivedKey, + definitions::{MFKDF2DerivedKey, MFKDF2Options}, derive, setup::{ self, @@ -94,7 +115,6 @@ mod tests { stack::StackOptions, uuid::UUIDOptions, }, - key::MFKDF2Options, }, }; @@ -284,7 +304,7 @@ mod tests { .unwrap(); let setup = setup::key( - &[setup::factors::ooba::ooba(OobaOptions { + &[setup::factors::ooba(OobaOptions { key: Some(jwk), params: Some(json!({ "email": "test@mfkdf.com" })), ..Default::default() @@ -305,7 +325,7 @@ mod tests { let derive = derive::key( &setup.policy, - HashMap::from([("ooba".to_string(), derive::factors::ooba::ooba(code).unwrap())]), + HashMap::from([("ooba".to_string(), derive::factors::ooba(code).unwrap())]), true, false, ) diff --git a/mfkdf2/src/error.rs b/mfkdf2/src/error.rs index e787ac11..dd954c9d 100644 --- a/mfkdf2/src/error.rs +++ b/mfkdf2/src/error.rs @@ -1,11 +1,16 @@ +//! Result and Error types for MFKDF2 operations. + +/// Result type for MFKDF2 operations. pub type MFKDF2Result = Result; -// TODO (autoparallel): It may be worth making this have inner errors, e.g., for factors and other -// things. That is usually not my style, but it may be nicer for the caller as long as destructuring -// the error is not too painful. +/// Error type for MFKDF2 operations. +#[allow(missing_docs)] #[cfg_attr(feature = "bindings", derive(uniffi::Error), uniffi(flat_error))] #[derive(thiserror::Error, Debug)] pub enum MFKDF2Error { + #[error("too many factors! maximum is 255")] + TooManyFactors, + #[error("password cannot be empty!")] PasswordEmpty, @@ -24,21 +29,12 @@ pub enum MFKDF2Error { #[error("factor id must be unique!")] DuplicateFactorId, - #[error(transparent)] - DecodeError(#[from] base64::DecodeError), - - #[error(transparent)] - RsaError(#[from] rsa::errors::Error), - - #[error(transparent)] - WriteError(#[from] std::fmt::Error), - // TODO (autoparallel): This error variant should probably not even exist. #[error("failed to convert vector to array!")] - TryFromVecError, + TryFromVec, #[error("share recovery failed!")] - ShareRecoveryError, + ShareRecovery, #[error("invalid key length")] InvalidKeyLength, @@ -101,8 +97,20 @@ pub enum MFKDF2Error { InvalidHintLength(&'static str), #[error(transparent)] - Argon2Error(#[from] argon2::Error), + Argon2(#[from] argon2::Error), + + #[error(transparent)] + Serialize(#[from] serde_json::Error), + + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + + #[error(transparent)] + Rsa(#[from] rsa::errors::Error), + + #[error(transparent)] + Write(#[from] std::fmt::Error), #[error(transparent)] - SerializeError(#[from] serde_json::Error), + Regex(#[from] rand_regex::Error), } diff --git a/mfkdf2/src/integrity.rs b/mfkdf2/src/integrity.rs index 903f0578..8b8d314c 100644 --- a/mfkdf2/src/integrity.rs +++ b/mfkdf2/src/integrity.rs @@ -1,8 +1,24 @@ +//! State integrity helpers for MFKDF2. +//! +//! To prevent an untrusted server (or an attacker) from silently changing the public "state" +//! containing the policy and factor configuration, the client authenticates it with a MAC (message +//! authentication code) computed using a key derived from the user's factors. +//! +//! The functions in this module turn a [`Policy`] and each [`PolicyFactor`] into SHA-256 +//! digests. These digests define exactly what bytes are covered by the MAC (for example, an +//! HMAC-SHA256 over the derived key and the extracted state). +//! +//! Any change to things like the threshold, KDF parameters, factor secrets, or salts will change +//! the MAC input and make verification fail. This defends against state-tampering attacks where an +//! attacker tries to weaken KDF parameters, modify or remove factors, or otherwise alter the public +//! state while still having it accepted as valid by an honest client. + use sha2::{Digest, Sha256}; -use crate::{policy::Policy, setup::key::PolicyFactor}; +use crate::policy::{Policy, PolicyFactor}; impl Policy { + /// Computes the digest of an entire policy, including all factors. pub fn extract(&self) -> [u8; 32] { let mut hasher = Sha256::new(); @@ -16,7 +32,7 @@ impl Policy { } } -/// Extracts the core signable content from a policy object. +/// Hashes the core policy fields that must remain stable. pub fn extract_policy_core(policy: &Policy) -> [u8; 32] { let mut hasher = Sha256::new(); diff --git a/mfkdf2/src/lib.rs b/mfkdf2/src/lib.rs index da0c597d..bc6c5a11 100644 --- a/mfkdf2/src/lib.rs +++ b/mfkdf2/src/lib.rs @@ -1,53 +1,18 @@ -#[cfg(feature = "bindings")] -uniffi::setup_scaffolding!(); +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] +#![warn(unused_extern_crates, unreachable_pub, nonstandard_style)] pub mod constants; -pub mod crypto; +mod crypto; pub mod definitions; pub mod derive; pub mod error; pub mod integrity; +mod log; pub mod otpauth; pub mod policy; -pub mod rng; +mod rng; pub mod setup; -use std::str::FromStr; - -type LogLevel = log::Level; - #[cfg(feature = "bindings")] -#[cfg_attr(feature = "bindings", uniffi::remote(Enum))] -enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, -} - -#[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn init_rust_logging(level: Option) { - // Determine log level from parameter or environment variable - let log_level: log::Level = if let Some(level) = level { - #[cfg(feature = "bindings")] - match level { - LogLevel::Trace => log::Level::Trace, - LogLevel::Debug => log::Level::Debug, - LogLevel::Info => log::Level::Info, - LogLevel::Warn => log::Level::Warn, - LogLevel::Error => log::Level::Error, - } - - #[cfg(not(feature = "bindings"))] - level - } else { - let env_level = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); - log::Level::from_str(&env_level).unwrap_or(log::Level::Info) - }; - - #[cfg(target_arch = "wasm32")] - let _ = console_log::init_with_level(log_level); - - log::set_max_level(log_level.to_level_filter()); -} +uniffi::setup_scaffolding!(); diff --git a/mfkdf2/src/log.rs b/mfkdf2/src/log.rs new file mode 100644 index 00000000..57c06005 --- /dev/null +++ b/mfkdf2/src/log.rs @@ -0,0 +1,61 @@ +//! Logging for the MFKDF2 library. +//! +//! This module is used to initialize the logging for the library. It is enabled by the +//! `bindings` feature flag. +#![allow(unused)] + +use std::str::FromStr; + +type LogLevel = log::Level; + +/// Logging level for MFKDF2 operations. +/// +/// This enum defines the available logging levels that can be used +/// to control the verbosity of log output in the MFKDF2 library. +#[cfg(feature = "bindings")] +#[cfg_attr(feature = "bindings", uniffi::remote(Enum))] +enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +/// Initialize logging for the MFKDF2 library. +/// +/// This function sets up logging with the specified level. If no level is provided, +/// it falls back to the `RUST_LOG` environment variable, defaulting to "info" level. +/// +/// # Arguments +/// +/// * `level` - Optional logging level. If None, uses `RUST_LOG` env var or defaults to Info. +/// +/// # Platform-specific behavior +/// On WASM targets, initializes `console_log`. On other platforms, sets the maximum log level +/// filter. +#[cfg_attr(feature = "bindings", uniffi::export)] +fn init_log(level: Option) { + // Determine log level from parameter or environment variable + let log_level: log::Level = if let Some(level) = level { + #[cfg(feature = "bindings")] + match level { + LogLevel::Trace => log::Level::Trace, + LogLevel::Debug => log::Level::Debug, + LogLevel::Info => log::Level::Info, + LogLevel::Warn => log::Level::Warn, + LogLevel::Error => log::Level::Error, + } + + #[cfg(not(feature = "bindings"))] + level + } else { + let env_level = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); + log::Level::from_str(&env_level).unwrap_or(log::Level::Info) + }; + + #[cfg(target_arch = "wasm32")] + let _ = console_log::init_with_level(log_level); + + log::set_max_level(log_level.to_level_filter()); +} diff --git a/mfkdf2/src/otpauth.rs b/mfkdf2/src/otpauth.rs index 04eed0ad..d6e94e52 100644 --- a/mfkdf2/src/otpauth.rs +++ b/mfkdf2/src/otpauth.rs @@ -1,3 +1,5 @@ +//! OTP Auth URL generation for TOTP and HOTP credentials compatible with Google Authenticator and +//! other OTP authenticators. use std::fmt::Write; use data_encoding::{BASE32_NOPAD, HEXLOWER}; @@ -8,9 +10,13 @@ use sha2::{Sha256, Sha512}; use crate::error::MFKDF2Error; +/// An OATH credential can be a TOTP (Time-based One-time Password) or a HOTP (HMAC-based One-time +/// Password). #[derive(Debug, Clone, Copy)] pub enum Kind { + /// TOTP (Time-based One-time Password) Totp, + /// HOTP (HMAC-based One-time Password) Hotp, } @@ -23,7 +29,8 @@ impl std::fmt::Display for Kind { } } -#[derive(Debug, Clone, Copy)] +/// Encoding for the secret used. +#[derive(Debug, Clone)] pub enum Encoding { /// Treat `secret` as raw ASCII bytes (e.g., "mysecret") Ascii, @@ -33,13 +40,17 @@ pub enum Encoding { Hex, } +/// The hash algorithm used by the credential #[cfg_attr(feature = "bindings", derive(uniffi::Enum))] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum HashAlgorithm { + /// SHA-1 #[serde(rename = "sha1")] Sha1, + /// SHA-256 #[serde(rename = "sha256")] Sha256, + /// SHA-512 #[serde(rename = "sha512")] Sha512, } @@ -54,35 +65,37 @@ impl std::fmt::Display for HashAlgorithm { } } +/// Options for generating an OTP Auth URI #[derive(Debug, Clone)] -pub struct SharedOptions { - pub encoding: Option, // default: Ascii - pub algorithm: Option, // default: Sha1 -} - -#[derive(Debug, Clone)] -pub struct OtpauthUrlOptions { - /// Shared secret (format depends on `encoding`) - pub secret: String, - /// Label: usually account email/username - pub label: String, - /// "totp" (default) or "hotp" - pub kind: Option, - /// Required for HOTP - pub counter: Option, - /// Issuer (provider name) - pub issuer: Option, - /// Digits (default 6) - pub digits: Option, - /// Period seconds (default 30; TOTP-only) - pub period: Option, - /// Shared (encoding + algorithm) - pub shared: Option, +pub struct OtpAuthUrlOptions { + /// The shared secret is the secret key that is used to generate the OTP. The format depends on + /// the encoding specified in the [`Encoding`] field + pub secret: String, + /// The label is used to identify which account a credential is associated with. It also serves + /// as the unique identifier for the credential itself + pub label: String, + /// Credential type, either [`Kind::Totp`] or [`Kind::Hotp`] + pub kind: Option, + /// The issuer parameter is an optional string value indicating the provider or service the + /// credential is associated with + pub issuer: Option, + /// The number of digits in the OTP. Allowed values are 6, 7, and 8. Defaults to 6 + pub digits: Option, + /// The optional counter parameter is required when provisioning HOTP credentials. It will set + /// the initial counter value + pub counter: Option, + /// The period parameter defines a validity period in seconds for the TOTP code. It is only + /// applicable for TOTP credentials and defaults to 30 seconds + pub period: Option, + /// The encoding of the secret + pub encoding: Option, + /// The hash algorithm to use for the credential + pub algorithm: Option, } /// 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 { +fn secret_to_base32_no_pad(secret: &str, enc: &Encoding) -> Result { match enc { Encoding::Ascii => { // Interpret characters literally as bytes, then Base32 encode @@ -106,15 +119,25 @@ fn secret_to_base32_no_pad(secret: &str, enc: Encoding) -> Result Result { - let enc = options.shared.as_ref().and_then(|s| s.encoding).unwrap_or(Encoding::Ascii); - let alg = options - .shared - .as_ref() - .and_then(|s| s.algorithm.clone()) - .unwrap_or(HashAlgorithm::Sha1) - .to_string() - .to_ascii_uppercase(); +/// Generates an OTP Auth URI compatible with Google Authenticator and other OTP authenticators. +/// The otpauth:// URI scheme is used to encode one-time password (OTP) secrets for use with +/// authenticator applications, typically encoded in QR codes for easy provisioning. +/// +/// # Arguments +/// +/// * `options` - The options for generating the OTP Auth URI. +/// +/// # Returns +/// +/// A string representing the OTP Auth URI. +/// +/// # Errors +/// +/// Returns an error if the secret is invalid or the options are missing required fields. +pub fn otpauth_url(options: &OtpAuthUrlOptions) -> Result { + let enc = options.encoding.as_ref().unwrap_or(&Encoding::Ascii); + let alg = + options.algorithm.as_ref().unwrap_or(&HashAlgorithm::Sha1).to_string().to_ascii_uppercase(); let digits = options.digits.unwrap_or(6); let period = options.period.unwrap_or(30); let kind = options.kind.unwrap_or(Kind::Totp); @@ -144,7 +167,15 @@ pub fn otpauth_url(options: &OtpauthUrlOptions) -> Result { Ok(url) } -pub fn generate_hotp_code(secret: &[u8], counter: u64, hash: &HashAlgorithm, digits: u32) -> u32 { +/// Generate a counter-based one-time token of the given length. +/// +/// # Arguments +/// +/// * `secret` - The shared secret is the secret key that is used to generate the OTP +/// * `counter` - The counter value to use for the OTP. +/// * `hash` - The hash algorithm to use for the OTP. +/// * `digits` - The number of digits in the OTP. +pub fn generate_otp_token(secret: &[u8], counter: u64, hash: &HashAlgorithm, digits: u32) -> u32 { let counter_bytes = counter.to_be_bytes(); let digest = match hash { @@ -183,18 +214,16 @@ mod tests { #[test] fn otpauth_url_totp() { - let options = OtpauthUrlOptions { - secret: "mysecret".to_string(), - label: "mylabel".to_string(), - kind: Some(Kind::Totp), - counter: Some(1), - issuer: Some("myissuer".to_string()), - digits: Some(6), - period: Some(30), - shared: Some(SharedOptions { - encoding: Some(Encoding::Ascii), - algorithm: Some(HashAlgorithm::Sha1), - }), + let options = OtpAuthUrlOptions { + secret: "mysecret".to_string(), + label: "mylabel".to_string(), + kind: Some(Kind::Totp), + counter: Some(1), + issuer: Some("myissuer".to_string()), + digits: Some(6), + period: Some(30), + encoding: Some(Encoding::Ascii), + algorithm: Some(HashAlgorithm::Sha1), }; let url = otpauth_url(&options).unwrap(); @@ -207,18 +236,16 @@ mod tests { #[test] fn otpauth_url_hotp() { - let options = OtpauthUrlOptions { - secret: "mysecret".to_string(), - label: "mylabel".to_string(), - kind: Some(Kind::Hotp), - counter: Some(1), - issuer: Some("myissuer".to_string()), - digits: Some(6), - period: None, - shared: Some(SharedOptions { - encoding: Some(Encoding::Ascii), - algorithm: Some(HashAlgorithm::Sha1), - }), + let options = OtpAuthUrlOptions { + secret: "mysecret".to_string(), + label: "mylabel".to_string(), + kind: Some(Kind::Hotp), + counter: Some(1), + issuer: Some("myissuer".to_string()), + digits: Some(6), + period: None, + encoding: Some(Encoding::Ascii), + algorithm: Some(HashAlgorithm::Sha1), }; let url = otpauth_url(&options).unwrap(); @@ -230,18 +257,16 @@ mod tests { #[test] fn otpauth_url_base32() { - let options = OtpauthUrlOptions { - secret: BASE32_NOPAD.encode(b"mysecret"), - label: "mylabel".to_string(), - kind: Some(Kind::Totp), - counter: Some(1), - issuer: Some("myissuer".to_string()), - digits: Some(6), - period: Some(30), - shared: Some(SharedOptions { - encoding: Some(Encoding::Base32), - algorithm: Some(HashAlgorithm::Sha1), - }), + let options = OtpAuthUrlOptions { + secret: BASE32_NOPAD.encode(b"mysecret"), + label: "mylabel".to_string(), + kind: Some(Kind::Totp), + counter: Some(1), + issuer: Some("myissuer".to_string()), + digits: Some(6), + period: Some(30), + encoding: Some(Encoding::Base32), + algorithm: Some(HashAlgorithm::Sha1), }; let url = otpauth_url(&options).unwrap(); @@ -254,18 +279,16 @@ mod tests { #[test] fn otpauth_url_hex() { - let options = OtpauthUrlOptions { - secret: hex::encode("mysecret"), - label: "mylabel".to_string(), - kind: Some(Kind::Totp), - counter: Some(1), - issuer: Some("myissuer".to_string()), - digits: Some(6), - period: Some(30), - shared: Some(SharedOptions { - encoding: Some(Encoding::Hex), - algorithm: Some(HashAlgorithm::Sha1), - }), + let options = OtpAuthUrlOptions { + secret: hex::encode("mysecret"), + label: "mylabel".to_string(), + kind: Some(Kind::Totp), + counter: Some(1), + issuer: Some("myissuer".to_string()), + digits: Some(6), + period: Some(30), + encoding: Some(Encoding::Hex), + algorithm: Some(HashAlgorithm::Sha1), }; let url = otpauth_url(&options).unwrap(); @@ -283,15 +306,15 @@ mod tests { let hash = HashAlgorithm::Sha1; let digits = 6; - let code = generate_hotp_code(secret, counter, &hash, digits); + let code = generate_otp_token(secret, counter, &hash, digits); assert!(code < 10_u32.pow(digits)); // Same inputs should produce same output - let code2 = generate_hotp_code(secret, counter, &hash, digits); + let code2 = generate_otp_token(secret, counter, &hash, digits); assert_eq!(code, code2); // Different counter should produce different output - let code3 = generate_hotp_code(secret, counter + 1, &hash, digits); + let code3 = generate_otp_token(secret, counter + 1, &hash, digits); assert_ne!(code, code3); } } diff --git a/mfkdf2/src/policy/derive.rs b/mfkdf2/src/policy/derive.rs index 84216577..68cbd584 100644 --- a/mfkdf2/src/policy/derive.rs +++ b/mfkdf2/src/policy/derive.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ definitions::{MFKDF2DerivedKey, MFKDF2Factor}, - derive::factors::stack::stack as create_stack_factor, + derive::factors::stack as derive_stack, error::{MFKDF2Error, MFKDF2Result}, policy::{Policy, evaluate::evaluate_internal}, }; @@ -20,7 +20,7 @@ fn expand( && evaluate_internal(&nested_policy, factor_set) { let nested_expanded = expand(&nested_policy, factors, factor_set)?; - let stack_factor = create_stack_factor(nested_expanded)?; + let stack_factor = derive_stack(nested_expanded)?; parsed_factors.insert(factor.id.clone(), stack_factor); } } else if factor_set.contains(&factor.id) @@ -33,6 +33,56 @@ fn expand( Ok(parsed_factors) } +/// Derives a key using the given policy and factors. +/// +/// # Arguments +/// +/// * `policy`: The [`Policy`] to use for the derivation. +/// * `factors`: Map of factor IDs to [`MFKDF2Factor`] implementations. Usually derived using policy +/// combinators ([`and`](`crate::policy::and`), [`or`](`crate::policy::or`), +/// [`all`](`crate::policy::all`), [`any`](`crate::policy::any`)). +/// * `verify`: Whether to verify the self-referential MFKDF2 policy integrity. Default is true. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// use mfkdf2::{ +/// derive::factors::password as derive_password, +/// policy::{PolicySetupOptions, and, derive, or, setup}, +/// setup::factors::password::{PasswordOptions, password}, +/// }; +/// let setup = setup( +/// and( +/// password("password1", PasswordOptions { id: Some("pwd1".into()) })?, +/// or( +/// password("password2", PasswordOptions { id: Some("pwd2".into()) })?, +/// password("password3", PasswordOptions { id: Some("pwd3".into()) })?, +/// )?, +/// )?, +/// PolicySetupOptions::default(), +/// )?; +/// +/// // Derive the key using the policy. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([ +/// ("pwd1".to_string(), derive_password("password1")?), +/// ("pwd2".to_string(), derive_password("password2")?), +/// ]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using invalid factors. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// ); +/// assert!(derived_key.is_err()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn derive( policy: &Policy, factors: &HashMap, @@ -49,11 +99,12 @@ pub fn derive( let expanded_factors = expand(policy, factors, &factor_set)?; - crate::derive::key::key(policy, expanded_factors, verify.unwrap_or(true), false) + crate::derive::key(policy, expanded_factors, verify.unwrap_or(true), false) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn policy_derive( +fn policy_derive( policy: &Policy, factors: &HashMap, verify: Option, diff --git a/mfkdf2/src/policy/evaluate.rs b/mfkdf2/src/policy/evaluate.rs index d62d3947..3c6e36e7 100644 --- a/mfkdf2/src/policy/evaluate.rs +++ b/mfkdf2/src/policy/evaluate.rs @@ -2,12 +2,50 @@ use std::collections::HashSet; use super::Policy; -pub fn evaluate(policy: &Policy, factor_ids: Vec) -> bool { - let factor_set: HashSet = factor_ids.into_iter().collect(); - evaluate_internal(policy, &factor_set) +impl Policy { + /// Evaluates the policy by checking if the given factor IDs are valid and sufficient to derive + /// the key. + /// + /// # Example + /// + /// ```rust + /// use mfkdf2::{ + /// policy, + /// policy::PolicySetupOptions, + /// setup::factors::password::{PasswordOptions, password}, + /// }; + /// # + /// let setup = policy::setup( + /// policy::and( + /// policy::or( + /// password("password1", PasswordOptions { id: Some("pwd1".into()) })?, + /// password("password2", PasswordOptions { id: Some("pwd2".into()) })?, + /// )?, + /// policy::or( + /// password("password3", PasswordOptions { id: Some("pwd3".into()) })?, + /// password("password4", PasswordOptions { id: Some("pwd4".into()) })?, + /// )?, + /// )?, + /// PolicySetupOptions::default(), + /// )?; + /// + /// // Evaluate the policy with the given factor IDs. + /// let is_valid = setup.policy.evaluate(vec![String::from("pwd1"), String::from("pwd4")]); + /// assert!(is_valid); + /// + /// // invalid policy combination + /// let is_valid = setup.policy.evaluate(vec![String::from("pwd1"), String::from("pwd2")]); + /// assert!(!is_valid); + /// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) + /// ``` + pub fn evaluate(&self, factor_ids: Vec) -> bool { + let factor_set: HashSet = factor_ids.into_iter().collect(); + evaluate_internal(self, &factor_set) + } } -pub(crate) fn evaluate_internal(policy: &Policy, factor_set: &HashSet) -> bool { +/// Recursively evaluates the policy by checking policy threshold is met by the given factor IDs. +pub(super) fn evaluate_internal(policy: &Policy, factor_set: &HashSet) -> bool { let threshold = policy.threshold; let mut actual = 0; @@ -26,7 +64,6 @@ pub(crate) fn evaluate_internal(policy: &Policy, factor_set: &HashSet) - actual >= threshold } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_evaluate"))] -pub fn policy_evaluate(policy: &Policy, factor_ids: Vec) -> bool { - evaluate(policy, factor_ids) -} +fn policy_evaluate(policy: &Policy, factor_ids: Vec) -> bool { policy.evaluate(factor_ids) } diff --git a/mfkdf2/src/policy/logic.rs b/mfkdf2/src/policy/logic.rs index 6b51d74b..4a9523a8 100644 --- a/mfkdf2/src/policy/logic.rs +++ b/mfkdf2/src/policy/logic.rs @@ -1,6 +1,7 @@ use crate::{definitions::MFKDF2Factor, error::MFKDF2Result, setup::factors::stack::StackOptions}; #[cfg(feature = "differential-test")] +/// Generates a deterministic stack ID based on the threshold and the sorted child factor IDs. fn factor_id(n: u8, factors: &[MFKDF2Factor]) -> String { use sha2::{Digest, Sha256}; // Deterministic stack id based on threshold and sorted child ids @@ -15,8 +16,44 @@ fn factor_id(n: u8, factors: &[MFKDF2Factor]) -> String { } #[cfg(not(feature = "differential-test"))] +/// Generates a random ID for the given group of factors. fn factor_id(_n: u8, _factors: &Vec) -> String { uuid::Uuid::new_v4().to_string() } +/// Derives a key with threshold of 1 among n factors. +/// +/// # Example +/// +/// ```rust +/// use std::collections::HashMap; +/// +/// use mfkdf2::{ +/// derive::factors::password as derive_password, +/// policy::{PolicySetupOptions, at_least, derive, setup}, +/// setup::factors::password::{PasswordOptions, password}, +/// }; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// let f3 = password("password3", PasswordOptions { id: Some("pwd3".into()) })?; +/// // Create a stack factor such that any one factor is sufficient to derive the key. +/// let setup = setup(at_least(1, vec![f1, f2, f3])?, PolicySetupOptions::default())?; +/// +/// // Derive the key using the stack factor. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd1".to_string(), derive_password("password1")?)]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using other factors. +/// let derived_key = derive( +/// &derived_key.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_at_least"))] pub fn at_least(n: u8, factors: Vec) -> MFKDF2Result { let id = factor_id(n, &factors); @@ -24,22 +61,168 @@ pub fn at_least(n: u8, factors: Vec) -> MFKDF2Result crate::setup::factors::stack(factors, options) } +/// Derives a key with threshold of 1 among 2 factors. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// # use mfkdf2::derive::{factors::password as derive_password}; +/// # use mfkdf2::policy::{or, setup, derive}; +/// # use mfkdf2::policy::PolicySetupOptions; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// +/// let setup = setup(or(f1, f2)?, PolicySetupOptions::default())?; +/// +/// // Derive the key using the stack factor. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd1".to_string(), derive_password("password1")?)]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using invalid factors. +/// let derived_key = derive( +/// &derived_key.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// ); +/// assert!(derived_key.is_err()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_or"))] pub fn or(factor1: MFKDF2Factor, factor2: MFKDF2Factor) -> MFKDF2Result { at_least(1, vec![factor1, factor2]) } +/// Derives a key with threshold of 2 among 2 factors. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// # use mfkdf2::derive::{factors::password as derive_password}; +/// # use mfkdf2::policy::{and, setup, derive}; +/// # use mfkdf2::policy::PolicySetupOptions; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// +/// let setup = setup(and(f1, f2)?, PolicySetupOptions::default())?; +/// +/// // Derive the key using the stack factor. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([ +/// ("pwd1".to_string(), derive_password("password1")?), +/// ("pwd2".to_string(), derive_password("password2")?), +/// ]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using invalid factors. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// ); +/// assert!(derived_key.is_err()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_and"))] pub fn and(factor1: MFKDF2Factor, factor2: MFKDF2Factor) -> MFKDF2Result { at_least(2, vec![factor1, factor2]) } +/// Derives a key with threshold of n among n factors. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// # use mfkdf2::derive::{factors::password as derive_password}; +/// # use mfkdf2::policy::{all, setup, derive}; +/// # use mfkdf2::policy::PolicySetupOptions; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// let f3 = password("password3", PasswordOptions { id: Some("pwd3".into()) })?; +/// +/// let setup = setup(all(vec![f1, f2, f3])?, PolicySetupOptions::default())?; +/// +/// // Derive the key using the stack factor. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([ +/// ("pwd1".to_string(), derive_password("password1")?), +/// ("pwd2".to_string(), derive_password("password2")?), +/// ("pwd3".to_string(), derive_password("password3")?), +/// ]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using invalid factors. +/// let derived_key = derive( +/// &derived_key.policy, +/// &HashMap::from([("pwd4".to_string(), derive_password("password4")?)]), +/// None, +/// ); +/// assert!(derived_key.is_err()); +/// +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_all"))] pub fn all(factors: Vec) -> MFKDF2Result { - assert!(factors.len() < 256, "Too many factors for policy"); let n = factors.len() as u8; at_least(n, factors) } +/// Derives a key with threshold of 1 among n factors. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// # use mfkdf2::derive::{factors::password as derive_password}; +/// # use mfkdf2::policy::{any, setup, derive}; +/// # use mfkdf2::policy::PolicySetupOptions; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// let f3 = password("password3", PasswordOptions { id: Some("pwd3".into()) })?; +/// +/// let setup = setup(any(vec![f1, f2, f3])?, PolicySetupOptions::default())?; +/// +/// // Derive the key using the stack factor. +/// let derive_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd1".to_string(), derive_password("password1")?)]), +/// None, +/// )?; +/// assert_eq!(derive_key.key, setup.key); +/// +/// // Derive the key using any of the factors. +/// let derive_key = derive( +/// &derive_key.policy, +/// &HashMap::from([("pwd2".to_string(), derive_password("password2")?)]), +/// None, +/// )?; +/// assert_eq!(derive_key.key, setup.key); +/// +/// // Derive the key using any of the factors. +/// let derive_key = derive( +/// &derive_key.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// )?; +/// assert_eq!(derive_key.key, setup.key); +/// +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_any"))] pub fn any(factors: Vec) -> MFKDF2Result { at_least(1, factors) } diff --git a/mfkdf2/src/policy/mod.rs b/mfkdf2/src/policy/mod.rs index a725f46d..f0faf31c 100644 --- a/mfkdf2/src/policy/mod.rs +++ b/mfkdf2/src/policy/mod.rs @@ -1,42 +1,95 @@ -pub mod derive; -pub mod evaluate; -pub mod logic; -pub mod setup; +//! Policy-based key derivation combines [key stacking](`mod@crate::setup::factors::stack`) and +//! [threshold key derivation](`crate::derive::key`) behind the scenes to allow keys to be +//! setup and derived using arbitrarily-complex policies combining a number of factors. +//! +//! Policy is a JSON schema that defines the allowed combinations of factors that can be used to +//! derive the final key. It is used to validate the policy and to ensure that the key is derived +//! using the allowed factors. +mod derive; +mod evaluate; +mod logic; +mod setup; + +pub use derive::derive; +pub use logic::{all, and, any, at_least, or}; +pub use setup::{PolicySetupOptions, setup}; #[cfg(test)] mod tests; use std::collections::HashSet; use serde::{Deserialize, Serialize}; -use serde_json as json; +use serde_json::Value; -use crate::setup::key::PolicyFactor; +/// Policy factor contains the public parameters (encrypted secret, factor share) , construction +/// parameters (like salt, params), and other auxiliary state (kind, hint). +// TODO (autoparallel): We probably can just use the MFKDF2Factor struct directly here. +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +pub struct PolicyFactor { + /// Unique identifier for the factor + pub id: String, + /// Factor type + #[serde(rename = "type")] + pub kind: String, + /// Base-64 encoded encrypted shamir share to recover the master secret + pub pad: String, + /// Base-64 encoded salt value used to derive the factor secret + pub salt: String, + /// Base-64 encrypted factor secret value used to reconstitute ke + pub secret: String, + /// Parameters required by the factor + // TODO (@lonerapier): convert it into a factor based enum + pub params: Value, + /// Optional [hint](`crate::definitions::mfkdf_derived_key::hints`) for the factor (in binary + /// string format) + #[serde(skip_serializing_if = "Option::is_none")] + pub hint: Option, +} +/// 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 +/// (params). +/// +/// See [`policy::setup`](`setup::setup`), [`policy::derive`](`derive::derive`) on how to derive a +/// policy enforced key. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct Policy { + /// JSON schema URL to validate the key policy. #[serde(rename = "$schema")] pub schema: String, + /// Unique identifier for the policy. #[serde(rename = "$id")] pub id: String, + /// Threshold for the policy. pub threshold: u8, + /// Base-64 encoded salt value used to derive the policy key. pub salt: String, + /// [`PolicyFactor`] combination used to derive the key in the policy. pub factors: Vec, + /// Base-64 encoded HMAC value used to verify the policy [integrity](`crate::integrity`). #[serde(skip_serializing_if = "String::is_empty")] #[serde(default = "String::new")] pub hmac: String, + /// Additional rounds of argon2 time cost to add, beyond OWASP minimums. pub time: u32, + /// Additional argon2 memory cost to add (in KiB), beyond OWASP minimums. pub memory: u32, + /// Base-64 encoded policy key encrypted using KEK (key encapsulation key). + /// It is used to derive other keys (params, integrity, etc.) in the policy. pub key: String, } impl Policy { + /// Returns a list of all factor IDs in the policy. pub fn ids(&self) -> Vec { let mut list: Vec = Vec::new(); for factor in &self.factors { list.push(factor.id.clone()); if factor.kind == "stack" - && let Ok(nested) = json::from_value::(factor.params.clone()) + && let Ok(nested) = serde_json::from_value::(factor.params.clone()) { list.extend(nested.ids()); } @@ -44,6 +97,7 @@ impl Policy { list } + /// Validates the policy by checking for duplicate factor IDs. pub fn validate(&self) -> bool { let list = self.ids(); let set: HashSet = list.iter().cloned().collect(); @@ -51,5 +105,6 @@ impl Policy { } } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_validate"))] -pub fn validate(policy: &Policy) -> bool { policy.validate() } +fn validate(policy: &Policy) -> bool { policy.validate() } diff --git a/mfkdf2/src/policy/setup.rs b/mfkdf2/src/policy/setup.rs index e8df55d3..ffeb4986 100644 --- a/mfkdf2/src/policy/setup.rs +++ b/mfkdf2/src/policy/setup.rs @@ -1,17 +1,22 @@ use serde::{Deserialize, Serialize}; use crate::{ - definitions::{MFKDF2DerivedKey, MFKDF2Factor, Salt}, + definitions::{MFKDF2DerivedKey, MFKDF2Factor, MFKDF2Options, Salt}, error::{MFKDF2Error, MFKDF2Result}, - setup::key::{MFKDF2Options, key as setup_key}, + setup::key as setup_key, }; +/// Options for setting up a policy. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct PolicySetupOptions { + /// Unique identifier for the policy. pub id: Option, + /// Threshold for the policy. pub threshold: Option, + /// Flag to perform integrity checks for the policy. pub integrity: Option, + /// 32 byte salt value used to derive the policy key. pub salt: Option, } @@ -39,6 +44,55 @@ impl From for MFKDF2Options { } } +/// Policy factor construction. Validates and setup a policy based multi-factor derived key. +/// +/// # Arguments +/// +/// * `factor`: [`MFKDF2Factor`] construction. Usually setup using policy combinators +/// ([`and`](`crate::policy::and`), [`or`](`crate::policy::or`), [`all`](`crate::policy::all`), +/// [`any`](`crate::policy::any`)). +/// * `options`: [`PolicySetupOptions`] to use for the setup. +/// +/// # Example +/// +/// ```rust +/// # use std::collections::HashMap; +/// use mfkdf2::{ +/// derive::factors::password as derive_password, +/// policy::{PolicySetupOptions, and, derive, or, setup}, +/// setup::factors::password::{PasswordOptions, password}, +/// }; +/// let setup = setup( +/// and( +/// password("password1", PasswordOptions { id: Some("pwd1".into()) })?, +/// or( +/// password("password2", PasswordOptions { id: Some("pwd2".into()) })?, +/// password("password3", PasswordOptions { id: Some("pwd3".into()) })?, +/// )?, +/// )?, +/// PolicySetupOptions::default(), +/// )?; +/// +/// // Derive the key using the policy. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([ +/// ("pwd1".to_string(), derive_password("password1")?), +/// ("pwd2".to_string(), derive_password("password2")?), +/// ]), +/// None, +/// )?; +/// assert_eq!(derived_key.key, setup.key); +/// +/// // Derive the key using invalid factors. +/// let derived_key = derive( +/// &setup.policy, +/// &HashMap::from([("pwd3".to_string(), derive_password("password3")?)]), +/// None, +/// ); +/// assert!(derived_key.is_err()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn setup(factor: MFKDF2Factor, options: PolicySetupOptions) -> MFKDF2Result { let derived_key = setup_key(&[factor], options.into())?; @@ -49,8 +103,9 @@ pub fn setup(factor: MFKDF2Factor, options: PolicySetupOptions) -> MFKDF2Result< Ok(derived_key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn policy_setup( +fn policy_setup( factor: MFKDF2Factor, options: PolicySetupOptions, ) -> MFKDF2Result { diff --git a/mfkdf2/src/policy/tests.rs b/mfkdf2/src/policy/tests.rs index 8a2877be..57dcb6c6 100644 --- a/mfkdf2/src/policy/tests.rs +++ b/mfkdf2/src/policy/tests.rs @@ -57,7 +57,7 @@ fn create_policy_derive_factor( let digits = params["digits"].as_u64().unwrap() as u32; let hash = serde_json::from_value(params["hash"].clone()).unwrap(); let secret = vec![0u8; 20]; - let code = otpauth::generate_hotp_code(&secret, counter, &hash, digits); + let code = otpauth::generate_otp_token(&secret, counter, &hash, digits); (id.to_string(), derive::factors::hotp(code).unwrap()) }, "totp" => { @@ -72,7 +72,7 @@ fn create_policy_derive_factor( let digits = params["digits"].as_u64().unwrap() as u32; let counter = time as u64 / (step * 1000); let secret = vec![0u8; 20]; - let code = otpauth::generate_hotp_code(&secret, counter, &hash, digits); + let code = otpauth::generate_otp_token(&secret, counter, &hash, digits); (id.to_string(), derive::factors::totp(code, None).unwrap()) }, _ => panic!("Unknown factor type: {}", name), @@ -178,10 +178,10 @@ fn validate_invalid() { fn evaluate_basic_1() { let policy = create_policy_basic_1(); - assert!(!policy::evaluate::evaluate(&policy, vec!["id1".to_string(), "id2".to_string()])); - assert!(!policy::evaluate::evaluate(&policy, vec!["id3".to_string(), "id4".to_string()])); - assert!(policy::evaluate::evaluate(&policy, vec!["id1".to_string(), "id4".to_string()])); - assert!(policy::evaluate::evaluate(&policy, vec!["id2".to_string(), "id3".to_string()])); + assert!(!policy.evaluate(vec!["id1".to_string(), "id2".to_string()])); + assert!(!policy.evaluate(vec!["id3".to_string(), "id4".to_string()])); + assert!(policy.evaluate(vec!["id1".to_string(), "id4".to_string()])); + assert!(policy.evaluate(vec!["id2".to_string(), "id3".to_string()])); } fn create_policy_basic_2() -> policy::Policy { @@ -212,10 +212,10 @@ fn create_policy_basic_2() -> policy::Policy { fn evaluate_basic_2() { let policy = create_policy_basic_2(); - assert!(policy::evaluate::evaluate(&policy, vec!["id1".to_string(), "id2".to_string()])); - assert!(policy::evaluate::evaluate(&policy, vec!["id3".to_string(), "id4".to_string()])); - assert!(!policy::evaluate::evaluate(&policy, vec!["id1".to_string(), "id4".to_string()])); - assert!(!policy::evaluate::evaluate(&policy, vec!["id2".to_string(), "id3".to_string()])); + assert!(policy.evaluate(vec!["id1".to_string(), "id2".to_string()])); + assert!(policy.evaluate(vec!["id3".to_string(), "id4".to_string()])); + assert!(!policy.evaluate(vec!["id1".to_string(), "id4".to_string()])); + assert!(!policy.evaluate(vec!["id2".to_string(), "id3".to_string()])); } #[test] diff --git a/mfkdf2/src/rng.rs b/mfkdf2/src/rng.rs index d41b6d9a..03cbb9cb 100644 --- a/mfkdf2/src/rng.rs +++ b/mfkdf2/src/rng.rs @@ -1,16 +1,31 @@ +//! # RNG +//! +//! RNG is used to generate random bytes for the MFKDF2 algorithm. It is implemented using the +//! `rand` crate. The reason for having a separate RNG module is to allow for differential testing. +//! +//! [`GlobalRng`] is a facade around the `rand` crate's [`rand::rngs::OsRng`] to provide the same +//! interface. +//! +//! ## Differential Testing +//! +//! Differential testing is used to ensure the correctness of the library. It is enabled by the +//! `differential-test` feature flag. It is performed by comparing the output of the library with +//! the output of the reference implementation. + #[cfg(feature = "differential-test")] -mod rng_impl { +mod global_rng { use std::cell::RefCell; use rand::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; + /// The default seed for the global RNG. const DEFAULT_SEED: [u8; 32] = [10u8; 32]; thread_local! { static RNG: RefCell = RefCell::new(ChaCha20Rng::from_seed(DEFAULT_SEED)); } - pub struct GlobalRng; + pub(crate) struct GlobalRng; impl RngCore for GlobalRng { fn next_u32(&mut self) -> u32 { RNG.with(|rng| rng.borrow_mut().next_u32()) } @@ -25,17 +40,21 @@ mod rng_impl { } impl CryptoRng for GlobalRng {} - pub fn fill_bytes(dst: &mut [u8]) { RNG.with(|rng| rng.borrow_mut().fill_bytes(dst)); } - pub fn next_u32() -> u32 { RNG.with(|rng| rng.borrow_mut().next_u32()) } - pub fn gen_range_u32(max: u32) -> u32 { if max == 0 { 0 } else { next_u32() % max } } - pub fn gen_range_u8(max: u8) -> u8 { if max == 0 { 0 } else { (next_u32() % max as u32) as u8 } } + pub(crate) fn fill_bytes(dst: &mut [u8]) { RNG.with(|rng| rng.borrow_mut().fill_bytes(dst)); } + pub(crate) fn next_u32() -> u32 { RNG.with(|rng| rng.borrow_mut().next_u32()) } + pub(crate) fn gen_range_u32(max: u32) -> u32 { if max == 0 { 0 } else { next_u32() % max } } + pub(crate) fn gen_range_u8(max: u8) -> u8 { + if max == 0 { 0 } else { (next_u32() % max as u32) as u8 } + } } #[cfg(not(feature = "differential-test"))] -mod rng_impl { +mod global_rng { use rand::{CryptoRng, RngCore, rngs::OsRng}; - pub struct GlobalRng; + /// [`GlobalRng`] is a facade around the `rand` crate's [`rand::rngs::OsRng`] to provide the same + /// interface. + pub(crate) struct GlobalRng; impl RngCore for GlobalRng { fn next_u32(&mut self) -> u32 { OsRng.next_u32() } @@ -50,15 +69,18 @@ mod rng_impl { } impl CryptoRng for GlobalRng {} - pub fn fill_bytes(dst: &mut [u8]) { OsRng.fill_bytes(dst); } - pub fn next_u32() -> u32 { OsRng.next_u32() } - pub fn gen_range_u32(max: u32) -> u32 { if max == 0 { 0 } else { next_u32() % max } } - pub fn gen_range_u8(max: u8) -> u8 { + pub(crate) fn fill_bytes(dst: &mut [u8]) { GlobalRng.fill_bytes(dst); } + + pub(crate) fn next_u32() -> u32 { GlobalRng.next_u32() } + + pub(crate) fn gen_range_u32(max: u32) -> u32 { if max == 0 { 0 } else { next_u32() % max } } + + pub(crate) fn gen_range_u8(max: u8) -> u8 { if max == 0 { 0 } else { (next_u32() % u32::from(max)) as u8 } } } -pub use rng_impl::*; +pub(crate) use global_rng::*; #[cfg(test)] mod tests { diff --git a/mfkdf2/src/setup/factors/hmacsha1.rs b/mfkdf2/src/setup/factors/hmacsha1.rs index ba5a6a6c..c36b4e23 100644 --- a/mfkdf2/src/setup/factors/hmacsha1.rs +++ b/mfkdf2/src/setup/factors/hmacsha1.rs @@ -1,3 +1,18 @@ +//! HMAC‑SHA1 factor setup. +//! +//! This factor models a hardware‑backed HMAC‑SHA1 secret (for example a key +//! stored in a `YubiKey` or smart card) as MFKDF2 factor material. +//! +//! At a high level: +//! - during **setup**, 160‑bit HMAC key `kₜ` is either provided or generated by MFKDF2), and an +//! initial challenge + encrypted pad is produced that can later be stored with the policy; +//! - during **derive**, the policy provides a fresh challenge for this factor. The challenge is +//! sent to the hardware token, collecting the 20‑byte HMAC‑SHA1 response, and passes it back to +//! MFKDF2 as [`HmacSha1Response`], which lets the library recover the same secret key material +//! without the token ever exposing `kₜ` again. +//! +//! The resulting factor gives you a stable 160‑bit secret that is only re‑derivable by a client +//! that can answer the HMAC‑SHA1 challenge‑response protocol. use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -9,10 +24,20 @@ use crate::{ setup::factors::{FactorMetadata, FactorSetup, FactorType}, }; +/// Options for configuring an [`HmacSha1`] factor. +/// +/// In a typical hardware‑token deployment you either: +/// - generate a 20‑byte key on the server (by leaving `secret` as `None`), then provision the +/// 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)] pub struct HmacSha1Options { + /// Optional application-defined identifier for the factor. Defaults to `"hmacsha1"`. If + /// provided, it must be non-empty. pub id: Option, + /// 20‑byte HMAC key. If omitted, a random key is generated. pub secret: Option>, } @@ -20,6 +45,7 @@ impl Default for HmacSha1Options { fn default() -> Self { Self { id: Some("hmacsha1".to_string()), secret: None } } } +/// HMAC‑SHA1 response #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HmacSha1Response(pub [u8; 20]); @@ -27,11 +53,19 @@ impl From<[u8; 20]> for HmacSha1Response { fn from(value: [u8; 20]) -> Self { HmacSha1Response(value) } } +/// HMAC‑SHA1 factor state. +/// +/// The `padded_secret` field stores the 20‑byte secret plus 12 bytes of random +/// padding. During setup, `params()` will derive a challenge and encrypted pad +/// 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)] 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, } @@ -67,6 +101,49 @@ impl FactorSetup for HmacSha1 { } } +/// Creates an HMAC‑SHA1 factor from the given options. +/// +/// Validates the id, ensures the secret is exactly 20 bytes (or generates a random one), and +/// returns an [`MFKDF2Factor`] with 160 bits of entropy. +/// +/// The factor is consumed by the derive side via +/// [`derive::hmacsha1`](`crate::derive::factors::hmacsha1`) which accepts an +/// [`HmacSha1Response`] (the 20‑byte HMAC‑SHA1 output from the hardware token for the current +/// challenge) and reconstructs the original secret. +/// +/// # Errors +/// - [`crate::error::MFKDF2Error::MissingFactorId`] if `id` is provided but empty. +/// - [`crate::error::MFKDF2Error::InvalidSecretLength`] if `secret` is supplied with a length other +/// than 20 bytes. +/// +/// When driving this factor through [`crate::derive::key`], derivation may additionally fail +/// with: +/// - [`crate::error::MFKDF2Error::MissingDeriveParams`] if the persisted policy params for this +/// factor are missing the `"pad"` field; +/// - [`crate::error::MFKDF2Error::InvalidDeriveParams`] if those params are malformed or cannot be +/// decoded (for example, if `"pad"` has been tampered with). +/// +/// # Example +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::MFKDF2Result, +/// setup::{ +/// self, +/// factors::hmacsha1::{HmacSha1Options, hmacsha1}, +/// }, +/// }; +/// +/// # const HMACSHA1_SECRET: [u8; 20] = [0x11; 20]; +/// // Setup: create a policy with a single HMAC‑SHA1 factor +/// let options = +/// HmacSha1Options { id: Some("token".into()), secret: Some(HMACSHA1_SECRET.to_vec()) }; +/// let factor = hmacsha1(options)?; +/// +/// let setup_key = setup::key(&[factor], MFKDF2Options::default())?; +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn hmacsha1(options: HmacSha1Options) -> MFKDF2Result { // Validation if let Some(ref id) = options.id @@ -97,8 +174,9 @@ pub fn hmacsha1(options: HmacSha1Options) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_hmacsha1(options: HmacSha1Options) -> MFKDF2Result { +async fn setup_hmacsha1(options: HmacSha1Options) -> MFKDF2Result { hmacsha1(options) } diff --git a/mfkdf2/src/setup/factors/hotp.rs b/mfkdf2/src/setup/factors/hotp.rs index fd22441f..a7407523 100644 --- a/mfkdf2/src/setup/factors/hotp.rs +++ b/mfkdf2/src/setup/factors/hotp.rs @@ -1,25 +1,67 @@ +//! Counter-based HOTP factor setup. +//! +//! This factor models a standard OATH HOTP "soft token" (e.g. Google Authenticator, 1Password, or +//! any RFC 4226 implementation) and the matching server-side verification logic. +//! +//! Conceptually: +//! - An authenticator app holds a shared HOTP key `hotkeyₜ` and counter `ctrₜ`. On each use it +//! displays a one-time code `otpₜ,ᵢ` that both client and server can compute given (hotkeyₜ, +//! ctrₜ,ᵢ). +//! - MFKDF2 needs a *fixed* piece of factor material σₜ rather than a changing OTP. For HOTP, each +//! dynamic code Wₜ,ᵢ = otpₜ,ᵢ is converted into a fixed secret integer `targetₜ` in the range [0, +//! 10ᵈ), where `d` is the number of digits in the code. +//! +//! During **setup**: +//! - sample a random integer `targetₜ` in [0, 10ᵈ) +//! - compute the first HOTP code `otpₜ,₀` using `hotkeyₜ` and counter ctrₜ,₀ = 1 +//! - store an offset offsetₜ,₀ = (targetₜ - otpₜ,₀) % 10ᵈ +//! - encrypt the padded HOTP secret under the final derived key `K` and expose it as the `"pad"` +//! field in the public params. +//! +//! The public HOTP parameters βₜ produced here (digits `d`, initial `counter`, `offset`, and the +//! encrypted `pad`) are what get embedded into the MFKDF2 policy. On the derive side, the client +//! sends a fresh HOTP code Wₜ,ᵢ = otpₜ,ᵢ, and the library reconstructs the same targetₜ using the +//! stored offset and counter, giving you stable factor material that is backward-compatible with +//! existing software tokens. +//! +//! Software-token based key-derivation constructions require no changes to existing authenticator +//! applications like Google Authenticator. Because the HOTP key hotkeyₜ is stored inside the factor +//! state βₜ (encrypted as the pad), the computation of new offset values happens entirely inside +//! the library's setup/derive machinery. The authenticator app is only ever asked to display otpₜ,ᵢ +//! once per login, exactly as it does today; it does not participate directly in the key-derivation +//! logic. use base64::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use crate::{ crypto::encrypt, - definitions::{FactorMetadata, FactorType, Key, MFKDF2Factor}, + definitions::{FactorType, Key, MFKDF2Factor, factor::FactorMetadata}, error::{MFKDF2Error, MFKDF2Result}, - otpauth::{self, HashAlgorithm, OtpauthUrlOptions, generate_hotp_code}, + otpauth::{self, HashAlgorithm, OtpAuthUrlOptions, generate_otp_token}, setup::FactorSetup, }; +/// Options for configuring a HOTP factor before setup #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HOTPOptions { + /// Optional application-defined identifier for the factor. Defaults to `"hotp"`. If + /// provided, it must be non-empty pub id: Option, // TODO (@lonerapier): use trait based type update for secret // Initially this should be 20 bytes, that later gets padded to 32 during construction. + /// 20‑byte HOTP secret. If omitted, a random secret is generated pub secret: Option>, + /// Number of digits in the OTP code (6–8). Values outside this range cause + /// [`MFKDF2Error::InvalidHOTPDigits`] pub digits: Option, + /// Hash algorithm used by the HOTP generator (default: SHA‑1) pub hash: Option, + /// A string value indicating the provider or service the credential is associated with. pub issuer: Option, + /// A string value identifying which account a credential is associated with. It also serves + /// as the unique identifier for the credential itself. pub label: Option, } @@ -36,14 +78,24 @@ impl Default for HOTPOptions { } } +/// HOTP configuration. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HOTPConfig { + /// Optional application-defined identifier for the factor. Defaults to `"hotp"`. If + /// provided, it must be non-empty pub id: String, + /// 20‑byte HOTP secret. If omitted, a random secret is generated pub secret: Vec, + /// Number of digits in the OTP code (6–8). Values outside this range cause + /// [`MFKDF2Error::InvalidHOTPDigits`] pub digits: u32, + /// Hash algorithm used by the HOTP generator (default: SHA‑1) pub hash: HashAlgorithm, + /// A string value indicating the provider or service the credential is associated with. pub issuer: String, + /// A string value identifying which account a credential is associated with. It also serves + /// as the unique identifier for the credential itself. pub label: String, } @@ -75,23 +127,34 @@ impl Default for HOTPConfig { } } +/// HOTP factor state. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HOTP { // TODO (@lonerapier): config is only used for setup, not for derive + /// HOTP configuration. pub config: HOTPConfig, + /// HOTP public parameters. pub params: Value, + /// HOTP code. pub code: u32, + /// HOTP factor material. The target code that is used to derive the key. pub target: u32, } +/// HOTP public parameters. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HOTPParams { + /// Hash algorithm used by the HOTP generator. pub hash: HashAlgorithm, + /// Number of digits in the OTP code. pub digits: u32, + /// Base64 encoded pad. pub pad: String, + /// HOTP counter. pub counter: u64, + /// Target - code offset. pub offset: u32, } @@ -108,7 +171,7 @@ impl FactorSetup for HOTP { fn params(&self, key: Key) -> MFKDF2Result { // Generate HOTP code with counter = 1 let code = - generate_hotp_code(&self.config.secret[..20], 1, &self.config.hash, self.config.digits); + generate_otp_token(&self.config.secret[..20], 1, &self.config.hash, self.config.digits); // Calculate offset let offset = @@ -137,7 +200,7 @@ impl FactorSetup for HOTP { "algorithm": self.config.hash.to_string(), "digits": self.config.digits, "counter": 1, - "uri": otpauth::otpauth_url(&OtpauthUrlOptions { + "uri": otpauth::otpauth_url(&OtpAuthUrlOptions { secret: hex::encode(&self.config.secret[..20]), label: self.config.label.clone(), kind: Some(otpauth::Kind::Hotp), @@ -145,19 +208,63 @@ impl FactorSetup for HOTP { issuer: Some(self.config.issuer.clone()), digits: Some(self.config.digits), period: None, - shared: Some(otpauth::SharedOptions { - encoding: Some(otpauth::Encoding::Hex), - algorithm: Some(self.config.hash.clone()), - }), + encoding: Some(otpauth::Encoding::Hex), + algorithm: Some(self.config.hash.clone()), }).unwrap() }) } } +/// Modulus operation to ensure the result is positive. #[inline] #[must_use] pub fn mod_positive(n: i64, m: i64) -> u32 { (((n % m) + m) % m) as u32 } +/// Initializes an HOTP factor from the given options. +/// +/// Validates the configuration, generates a random target code and (optionally) secret, and returns +/// an [`MFKDF2Factor`] that can participate in MFKDF2 key setup. The factor's `output()` method +/// exposes an `otpauth://` URI that you can display as a QR code for users. +/// +/// # Errors +/// +/// - [`MFKDF2Error::MissingFactorId`] if `id` is provided but empty. +/// - [`MFKDF2Error::InvalidHOTPDigits`] if `digits` is set outside `6..=8`. +/// - [`MFKDF2Error::InvalidSecretLength`] if `secret` is provided but not exactly 20 bytes. +/// +/// # Example +/// +/// Pairing with an authenticator app using a known secret: +/// +/// ```rust +/// use mfkdf2::{ +/// otpauth::HashAlgorithm, +/// setup::factors::hotp::{HOTPOptions, hotp}, +/// }; +/// +/// let options = HOTPOptions { +/// id: Some("login-hotp".into()), +/// secret: Some(b"shared-hotp-secret!!".to_vec()), // 20 bytes +/// digits: Some(6), +/// hash: Some(HashAlgorithm::Sha1), +/// issuer: Some("ExampleApp".into()), +/// label: Some("user@example.com".into()), +/// }; +/// let factor = hotp(options)?; +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Invalid digits +/// +/// ```rust +/// # use mfkdf2::setup::factors::hotp::{hotp, HOTPOptions}; +/// # use mfkdf2::otpauth::HashAlgorithm; +/// +/// let options = HOTPOptions { digits: Some(4), ..Default::default() }; +/// let result = hotp(options); +/// assert!(matches!(result, Err(mfkdf2::error::MFKDF2Error::InvalidHOTPDigits))); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn hotp(options: HOTPOptions) -> MFKDF2Result { let mut options = options; @@ -216,8 +323,9 @@ pub fn hotp(options: HOTPOptions) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_hotp(options: HOTPOptions) -> MFKDF2Result { hotp(options) } +async fn setup_hotp(options: HOTPOptions) -> MFKDF2Result { hotp(options) } #[cfg(test)] mod tests { @@ -357,7 +465,7 @@ mod tests { let offset = params["offset"].as_u64().unwrap() as u32; - let code = generate_hotp_code( + let code = generate_otp_token( &hotp_factor.config.secret[..20], 1, &hotp_factor.config.hash, diff --git a/mfkdf2/src/setup/factors/mod.rs b/mfkdf2/src/setup/factors/mod.rs index de2d7aae..6d7e240f 100644 --- a/mfkdf2/src/setup/factors/mod.rs +++ b/mfkdf2/src/setup/factors/mod.rs @@ -1,3 +1,10 @@ +//! # Factor Setup +//! +//! Every [`MFKDF2Factor`](`crate::definitions::MFKDF2Factor`) instance is constructed using Witness +//! Wᵢ and parameters βᵢ. Each factor performs `FactorSetup` that takes secret material σᵢ to +//! produce the initial parameters β₀ given some configuration and randomly generated static source +//! material κᵢ. The factor’s public state βᵢ then stores an encrypted version of σᵢ (using the key +//! feedback mechanism) and public helper data. pub mod hmacsha1; pub mod hotp; pub mod ooba; @@ -10,6 +17,7 @@ pub mod uuid; pub use hmacsha1::hmacsha1; pub use hotp::hotp; +pub use ooba::ooba; pub use passkey::passkey; pub use password::password; pub use question::question; @@ -19,13 +27,14 @@ pub use totp::totp; pub use uuid::uuid; use crate::{ - definitions::{FactorMetadata, FactorType, Key}, + definitions::{FactorType, Key, factor::FactorMetadata}, error::MFKDF2Result, setup::FactorSetup, }; impl FactorType { - pub fn setup(&self) -> &dyn FactorSetup { + /// Returns the setup implementation for the factor type. + pub(crate) fn setup(&self) -> &dyn FactorSetup { match self { FactorType::Password(password) => password, FactorType::HOTP(hotp) => hotp, @@ -51,19 +60,22 @@ impl FactorSetup for FactorType { fn output(&self) -> Self::Output { self.setup().output() } } -// Standalone exported functions for FFI +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn factor_type_kind(factor_type: &FactorType) -> String { factor_type.kind() } +fn factor_type_kind(factor_type: &FactorType) -> String { factor_type.kind() } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn factor_type_bytes(factor_type: &FactorType) -> Vec { factor_type.bytes() } +fn factor_type_bytes(factor_type: &FactorType) -> Vec { factor_type.bytes() } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn setup_factor_type_params(factor_type: &FactorType, key: Option) -> MFKDF2Result { +fn setup_factor_type_params(factor_type: &FactorType, key: Option) -> MFKDF2Result { // TODO (@lonerapier): remove dummy key usage let key = key.unwrap_or_else(|| [0u8; 32].into()); factor_type.params(key) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub fn setup_factor_type_output(factor_type: &FactorType) -> Value { factor_type.output() } +fn setup_factor_type_output(factor_type: &FactorType) -> Value { factor_type.output() } diff --git a/mfkdf2/src/setup/factors/ooba.rs b/mfkdf2/src/setup/factors/ooba.rs index fbf3773a..7c64c96f 100644 --- a/mfkdf2/src/setup/factors/ooba.rs +++ b/mfkdf2/src/setup/factors/ooba.rs @@ -1,3 +1,29 @@ +//! Out‑of‑band (OOBA) challenge factor setup. +//! +//! This factor is designed for flows where the user confirms a login or recovery +//! action on a **separate channel** (for example, push notification, e‑mail link, +//! or SMS) using a short alphanumeric code. Instead of treating the OOBA channel +//! itself as a secret, MFKDF2 derives factor material from a random 32‑byte target +//! and uses the channel only to transport a fresh one‑time code and an encrypted +//! payload. +//! +//! Conceptually: +//! - an OOBA service holds a public key pkₒ for the delivery channel, such as the recipient’s +//! S/MIME key or an SMS gateway RSA key +//! - during **setup**, the library samples a fixed integer targetₒ and an initial OOBA code otpₒ,₀ +//! of `d` digits, both chosen uniformly from the range [0, 10ᵈ) +//! - the modular difference offsetₒ,₀ = (targetₒ − otpₒ,₀) % 10ᵈ is stored along with an encrypted +//! code ciphertext ctₒ,₀ under pkₒ +//! - the public parameters βₒ,₀ embed `(d, pkₒ, offsetₒ,₀, ctₒ,₀)` into the policy +//! +//! During derive, the user receives a new OOBA challenge on the secondary channel +//! and submits the corresponding code Wₒ,ᵢ = otpₒ,ᵢ to the application. Using the +//! stored offset for that step, the factor reconstructs a stable secret +//! σₒ = (offsetₒ,ᵢ + Wₒ,ᵢ) % 10ᵈ and derives the same key material as in setup, +//! while at the same time encrypting the next challenge payload for the following +//! login. Even if the OOBA channel is only partially trusted, the resulting factor +//! material remains uniformly distributed and provides the same information‑theoretic +//! guarantees as the HOTP and TOTP constructions. use base64::{Engine, engine::general_purpose}; use jsonwebtoken::jwk::Jwk; use rand::rngs::OsRng; @@ -8,11 +34,12 @@ use sha2::Sha256; use crate::{ crypto::{encrypt, hkdf_sha256_with_info}, - definitions::{FactorMetadata, FactorType, Key, MFKDF2Factor}, + definitions::{FactorType, Key, MFKDF2Factor, factor::FactorMetadata}, error::{MFKDF2Error, MFKDF2Result}, setup::FactorSetup, }; +/// Generates a random alphanumeric string of the given length. #[inline] #[must_use] pub fn generate_alphanumeric_characters(length: u32) -> String { @@ -24,14 +51,24 @@ pub fn generate_alphanumeric_characters(length: u32) -> String { .collect() } +/// Wrapper for the RSA public key used to encrypt the next OOBA payload pub struct OobaPublicKey(pub RsaPublicKey); +/// Options for configuring an OOBA factor #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OobaOptions { + /// Optional application-defined identifier for the factor, defaults to `"ooba"` and must be + /// non-empty if provided pub id: Option, + /// Number of alphanumeric characters in the OOBA code in the range 1–32, otherwise + /// [`MFKDF2Error::InvalidOobaLength`] is returned pub length: Option, + /// RSA public key as a JWK used to encrypt the next OOBA payload; must be an RSA key or + /// [`MFKDF2Error::InvalidOobaKey`] is returned pub key: Option, + /// Arbitrary JSON metadata re‑encrypted and sent to the OOBA service such as user id, device id, + /// or display message pub params: Option, } @@ -41,13 +78,21 @@ impl Default for OobaOptions { } } +/// OOBA factor state #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Ooba { + /// Randomly generated 32‑byte target used as the factor’s underlying secret + // TODO (@lonerapier): use uniffi custom type pub target: Vec, + /// Number of alphanumeric characters in the OOBA code bound to this factor pub length: u8, + /// OOBA factor material as the last issued code value pub code: String, + /// RSA public key used to encrypt the next OOBA payload pub jwk: Option, + /// Arbitrary JSON metadata re‑encrypted and sent to the OOBA service such as user id, device id, + /// or display message pub params: Value, } @@ -66,12 +111,12 @@ impl TryFrom<&Jwk> for OobaPublicKey { let n = rsa::BigUint::from_bytes_be( &base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(n_str) - .map_err(MFKDF2Error::DecodeError)?, + .map_err(MFKDF2Error::Base64Decode)?, ); let e = rsa::BigUint::from_bytes_be( &base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(e_str) - .map_err(MFKDF2Error::DecodeError)?, + .map_err(MFKDF2Error::Base64Decode)?, ); Ok(OobaPublicKey(RsaPublicKey::new(n, e).map_err(|_| MFKDF2Error::InvalidOobaKey)?)) } @@ -110,6 +155,55 @@ impl FactorSetup for Ooba { } } +/// Creates an OOBA factor from the given options +/// +/// This helper validates the configuration, generates a random 32‑byte target, +/// and returns an [`MFKDF2Factor`] whose entropy estimate depends on the number +/// of alphanumeric characters in the code. During setup, `params()` generates a +/// fresh user‑visible OOBA code, derives a one‑time key from that code to pad +/// the internal target, and produces an RSA‑encrypted payload embedding the +/// application’s `params` plus the code for delivery through the out‑of‑band +/// channel. +/// +/// # Errors +/// - [`MFKDF2Error::MissingFactorId`] if `id` is provided but empty +/// - [`MFKDF2Error::InvalidOobaLength`] if `length` is `0` or greater than `32` +/// - [`MFKDF2Error::MissingOobaKey`] if no RSA JWK is provided +/// - [`MFKDF2Error::InvalidOobaKey`] if the provided JWK is not an RSA key +/// +/// # Example +/// +/// ```rust +/// use mfkdf2::setup::factors::ooba::{ooba, OobaOptions}; +/// use jsonwebtoken::jwk::Jwk; +/// # let jwk: Jwk = serde_json::from_str(r#"{"kty":"RSA","n":"...","e":"AQAB"}"#).unwrap(); +/// let options = OobaOptions { +/// id: Some("push-ooba".into()), +/// length: Some(8), +/// key: Some(jwk), +/// params: Some(serde_json::json!({"channel": "push", "device": "phone-1"})), +/// }; +/// let factor = ooba(options)?; +/// assert_eq!(factor.id.as_deref(), Some("push-ooba")); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Invalid length +/// +/// ```rust +/// use mfkdf2::setup::factors::ooba::{ooba, OobaOptions}; +/// use jsonwebtoken::jwk::Jwk; +/// # let jwk: Jwk = serde_json::from_str(r#"{"kty":"RSA","alg":"RSA-OAEP-256","n":"AA","e":"AQAB"}"#).unwrap(); +/// # use mfkdf2::error::MFKDF2Error; +/// let options = OobaOptions { +/// length: Some(40), // outside the allowed 1–32 range +/// key: Some(jwk), +/// ..Default::default() +/// }; +/// let result = ooba(options); +/// assert!(matches!(result, Err(MFKDF2Error::InvalidOobaLength))); +/// # Ok::<(), MFKDF2Error>(()) +/// ``` pub fn ooba(options: OobaOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id @@ -146,8 +240,9 @@ pub fn ooba(options: OobaOptions) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_ooba(options: OobaOptions) -> MFKDF2Result { ooba(options) } +async fn setup_ooba(options: OobaOptions) -> MFKDF2Result { ooba(options) } #[cfg(test)] mod tests { diff --git a/mfkdf2/src/setup/factors/passkey.rs b/mfkdf2/src/setup/factors/passkey.rs index 66d7f2bf..e68e3963 100644 --- a/mfkdf2/src/setup/factors/passkey.rs +++ b/mfkdf2/src/setup/factors/passkey.rs @@ -1,14 +1,38 @@ +//! Passkey factor setup. +//! +//! This factor is intended for **hardware‑backed credentials** such as `WebAuthn` +//! passkeys bound to a platform authenticator. Instead of consuming a traditional +//! `WebAuthn` signature (which is intentionally non‑deterministic to prevent replay +//! attacks), the factor expects a stable 32‑byte secret produced by the `WebAuthn` +//! PRF extension or an equivalent key‑derivation mechanism. +//! +//! Conceptually: +//! - a passkey authenticator holds a signing key on a curve such as secp256r1 and exposes a PRF +//! interface `prf(challenge) → prf_key` +//! - during registration, the relying party requests a PRF evaluation on a fixed challenge value, +//! yielding a deterministic 32‑byte `prf_key` +//! - that `prf_key` is stored by the application as passkey factor material and wrapped here as +//! high‑entropy input to MFKDF2 +//! +//! Because the same PRF evaluation is used in both setup and derive, the factor +//! behaves like a constant‑entropy hardware token: as long as the user unlocks +//! the passkey and the authenticator returns the same 32‑byte value, the MFKDF2 +//! key derivation receives identical factor bytes. + use serde::{Deserialize, Serialize}; use crate::{ - definitions::{FactorMetadata, FactorType, MFKDF2Factor}, - error::{MFKDF2Error, MFKDF2Result}, + definitions::{FactorType, MFKDF2Factor, factor::FactorMetadata}, + error::MFKDF2Result, setup::FactorSetup, }; +/// Passkey factor state #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Passkey { + /// 32‑byte secret derived from the passkey’s `WebAuthn` PRF output or equivalent + /// hardware‑protected key pub secret: Vec, } @@ -23,9 +47,12 @@ impl FactorSetup for Passkey { type Params = serde_json::Value; } +/// Options for configuring a passkey factor #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PasskeyOptions { + /// Optional application-defined identifier for the factor. Defaults to `"passkey"`. If + /// provided, it must be non-empty. pub id: Option, } @@ -33,6 +60,33 @@ impl Default for PasskeyOptions { fn default() -> Self { Self { id: Some("passkey".to_string()) } } } +/// Creates a passkey factor from a 32‑byte secret +/// +/// This constructor is intended for flows where a client or middleware layer has +/// already obtained a deterministic 32‑byte value from a `WebAuthn` PRF operation +/// or similar hardware‑backed primitive. The function does not perform any `WebAuthn` +/// protocol steps; it only validates the logical factor identifier and wraps the +/// provided secret as MFKDF2 factor material with a fixed 256‑bit entropy estimate. +/// +/// # Errors +/// - [`MFKDF2Error::MissingFactorId`](`crate::error::MFKDF2Error::MissingFactorId`) if `options.id` +/// is present but empty +/// +/// # Example +/// +/// ```rust +/// use mfkdf2::setup::factors::passkey::{PasskeyOptions, passkey}; +/// use rand::{RngCore, rngs::OsRng}; +/// # +/// // application stores a per‑credential PRF output from the platform authenticator +/// let mut prf = [0u8; 32]; +/// OsRng.fill_bytes(&mut prf); +/// +/// let factor = passkey(prf, PasskeyOptions::default())?; +/// assert_eq!(factor.id.as_deref(), Some("passkey")); +/// assert_eq!(factor.data().len(), 32); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn passkey(secret: [u8; 32], options: PasskeyOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id @@ -49,10 +103,11 @@ pub fn passkey(secret: [u8; 32], options: PasskeyOptions) -> MFKDF2Result, options: PasskeyOptions) -> MFKDF2Result { +async fn setup_passkey(secret: Vec, options: PasskeyOptions) -> MFKDF2Result { if secret.len() != 32 { - return Err(MFKDF2Error::InvalidSecretLength("passkey".to_string())); + return Err(crate::error::MFKDF2Error::InvalidSecretLength("passkey".to_string())); } passkey(secret.try_into().unwrap(), options) @@ -65,6 +120,6 @@ mod tests { #[test] fn passkey_errors() { let factor = passkey([0u8; 32], PasskeyOptions { id: Some("".to_string()) }); - assert!(matches!(factor, Err(MFKDF2Error::MissingFactorId))); + assert!(matches!(factor, Err(crate::error::MFKDF2Error::MissingFactorId))); } } diff --git a/mfkdf2/src/setup/factors/password.rs b/mfkdf2/src/setup/factors/password.rs index fc4d522c..ceecddf4 100644 --- a/mfkdf2/src/setup/factors/password.rs +++ b/mfkdf2/src/setup/factors/password.rs @@ -1,16 +1,23 @@ +//! Password-based factor setup. +//! +//! This factor turns a user-chosen password into MFKDF2 factor material. The factor also records +//! an entropy estimate derived from Dropbox's [`mod@zxcvbn`] crate, which can be used to enforce +//! password strength policies. use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use zxcvbn::zxcvbn; use crate::{ - definitions::{FactorMetadata, FactorType, MFKDF2Factor}, + definitions::{FactorType, MFKDF2Factor, factor::FactorMetadata}, error::{MFKDF2Error, MFKDF2Result}, setup::FactorSetup, }; +/// Password factor state #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Password { + /// User-chosen password string. pub password: String, } @@ -31,12 +38,46 @@ impl FactorSetup for Password { } } +/// Options for setting up a password factor. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct PasswordOptions { + /// Optional application-defined identifier for the factor. Defaults to `"password"`. If + /// provided, it must be non-empty. pub id: Option, } +/// Creates a password factor. +/// +/// This helper normalizes and validates the password, computes its entropy using +/// `zxcvbn`, and wraps it in an [`MFKDF2Factor`]. The resulting factor can be used +/// directly in `setup_stack` or in single-factor keys. +/// +/// # Errors +/// - [`MFKDF2Error::PasswordEmpty`] if `password` is empty. +/// - [`MFKDF2Error::MissingFactorId`] if `options.id` is present but empty. +/// +/// # Examples +/// +/// Basic usage with a default id: +/// +/// ```rust +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// let factor = password("correct horse battery staple", PasswordOptions::default())?; +/// assert_eq!(factor.id.as_deref(), Some("password")); +/// assert!(factor.entropy.unwrap() > 40.0); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Using a custom id so you can distinguish multiple password factors: +/// +/// ```rust +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// let options = PasswordOptions { id: Some("login-password".to_string()) }; +/// let factor = password("my login secret", options)?; +/// assert_eq!(factor.id.as_deref(), Some("login-password")); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn password( password: impl Into, options: PasswordOptions, @@ -61,11 +102,9 @@ pub fn password( }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_password( - password: String, - options: PasswordOptions, -) -> MFKDF2Result { +async fn setup_password(password: String, options: PasswordOptions) -> MFKDF2Result { crate::setup::factors::password::password(password, options) } @@ -103,14 +142,13 @@ mod tests { fn password_valid() { let factor = password("hello", PasswordOptions { id: None }).unwrap(); assert_eq!(factor.id, Some("password".to_string())); - match &factor.factor_type { - FactorType::Password(p) => { - assert_eq!(p.password, "hello"); - assert_eq!(p.bytes(), "hello".as_bytes()); - let params = p.params([0; 32].into()).unwrap(); - assert_eq!(params, json!({})); - }, + let p = match &factor.factor_type { + FactorType::Password(p) => p, _ => panic!("Wrong factor type"), - } + }; + assert_eq!(p.password, "hello"); + assert_eq!(p.bytes(), "hello".as_bytes()); + let params = p.params([0; 32].into()).unwrap(); + assert_eq!(params, json!({})); } } diff --git a/mfkdf2/src/setup/factors/question.rs b/mfkdf2/src/setup/factors/question.rs index f06b1df6..752aa7e1 100644 --- a/mfkdf2/src/setup/factors/question.rs +++ b/mfkdf2/src/setup/factors/question.rs @@ -1,17 +1,27 @@ +//! Question factor setup. +//! +//! This factor models a user-chosen security question and answer. The factor also records +//! an entropy estimate derived from Dropbox's [`mod@zxcvbn`] crate, which can be used to enforce +//! security question strength policies. use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use zxcvbn::zxcvbn; use crate::{ - definitions::{FactorMetadata, FactorType, Key, MFKDF2Factor}, + definitions::{FactorType, Key, MFKDF2Factor, factor::FactorMetadata}, error::{MFKDF2Error, MFKDF2Result}, setup::FactorSetup, }; +/// Options for configuring a security‑question factor. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct QuestionOptions { + /// Optional application-defined identifier for the factor. Defaults to `"question"`. If + /// provided, it must be non-empty. pub id: Option, + /// Human‑readable prompt shown to the user (e.g., _"What is your favorite teacher's name?"_). + /// If omitted, you can store or render the question separately in your application. pub question: Option, } @@ -19,11 +29,15 @@ impl Default for QuestionOptions { fn default() -> Self { Self { id: Some("question".to_string()), question: None } } } +/// Security‑question factor state. This factor models a user-chosen security question and answer. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Question { + /// Normalized answer string used as factor material. pub answer: String, + /// Factor public state that is used to derive the factor material. pub params: Value, + /// Human‑readable prompt shown to the user (e.g., _"What is your favorite teacher's name?"_). pub question: String, } @@ -50,6 +64,28 @@ impl FactorSetup for Question { } } +/// Creates a [`Question`] factor from a raw user answer. +/// +/// The `answer` is normalized (lower‑cased, punctuation removed, surrounding whitespace trimmed) +/// to reduce accidental mismatches. Entropy is computed using `zxcvbn` on the normalized answer and +/// stored on the factor. +/// +/// # Errors +/// - [`MFKDF2Error::AnswerEmpty`] if the provided answer is empty. +/// - [`MFKDF2Error::MissingFactorId`] if `options.id` is present but empty. +/// +/// # Example +/// +/// ```rust +/// # use mfkdf2::setup::factors::question::{question, QuestionOptions}; +/// let opts = QuestionOptions { +/// id: Some("recovery-question".into()), +/// question: Some("What is your favorite color?".into()), +/// }; +/// let factor = question("Blue! No, Yellow!", opts)?; +/// assert_eq!(factor.id.as_deref(), Some("recovery-question")); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn question(answer: impl Into, options: QuestionOptions) -> MFKDF2Result { let answer = answer.into(); if answer.is_empty() { @@ -83,11 +119,9 @@ pub fn question(answer: impl Into, options: QuestionOptions) -> MFKDF2Re }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_question( - answer: String, - options: QuestionOptions, -) -> MFKDF2Result { +async fn setup_question(answer: String, options: QuestionOptions) -> MFKDF2Result { question(answer, options) } diff --git a/mfkdf2/src/setup/factors/stack.rs b/mfkdf2/src/setup/factors/stack.rs index 2e163cb2..5200c40e 100644 --- a/mfkdf2/src/setup/factors/stack.rs +++ b/mfkdf2/src/setup/factors/stack.rs @@ -1,22 +1,31 @@ +//! Stack factor setup +//! +//! A stack factor wraps an entire MFKDF2 key (built from one or more underlying factors) as a +//! **single reusable factor**. This is useful when you want to derive a key once from a complex +//! policy and then treat that key as another factor in a higher‑level policy or protocol. use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::{ - definitions::{FactorMetadata, FactorType, Key, MFKDF2DerivedKey, MFKDF2Factor, Salt}, - error::{MFKDF2Error, MFKDF2Result}, - setup::{ - FactorSetup, - key::{self, MFKDF2Options}, + definitions::{ + FactorType, Key, MFKDF2DerivedKey, MFKDF2Factor, MFKDF2Options, Salt, factor::FactorMetadata, }, + error::{MFKDF2Error, MFKDF2Result}, + setup::{FactorSetup, key}, }; +/// Options for constructing a stack factor. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct StackOptions { + /// Optional application-defined identifier for the factor. Defaults to `"stack"`. If + /// provided, it must be non-empty pub id: Option, + /// Number of underlying factors that must be present to derive the stacked key pub threshold: Option, + /// Optional override for the policy salt. If not provided, a random salt will be generated. pub salt: Option, } @@ -36,10 +45,16 @@ impl From for MFKDF2Options { } } +/// Stack factor state. +/// +/// Contains both the derived key and a map of the underlying factors keyed by +/// their ids. The factor bytes are the derived key material. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Stack { + /// Map of underlying factors keyed by their ids. pub factors: HashMap, + /// Final Derived key. pub key: MFKDF2DerivedKey, } @@ -60,6 +75,32 @@ impl FactorSetup for Stack { fn output(&self) -> Self::Output { serde_json::to_value(&self.key).unwrap() } } +/// Creates a stack factor from existing factors. +/// +/// Internally this calls [`key::key`] to build an `MFKDF2DerivedKey` over the +/// provided factors and then packages the result as a single [`MFKDF2Factor`]. +/// This can fail if the inputs cannot form a valid policy (for example, an empty +/// factor list or an impossible threshold). +/// +/// # Errors +/// - [`MFKDF2Error::MissingFactorId`] if `id` is provided but empty. +/// - Any error returned by [`key::key`] when building the underlying policy. +/// +/// # Example +/// +/// ```rust +/// # use mfkdf2::setup::factors::password::{password, PasswordOptions}; +/// # use mfkdf2::setup::factors::stack::{stack, StackOptions}; +/// let f1 = password("password1", PasswordOptions { id: Some("pwd1".into()) })?; +/// let f2 = password("password2", PasswordOptions { id: Some("pwd2".into()) })?; +/// let stacked = stack(vec![f1, f2], StackOptions { +/// id: Some("my-stack".into()), +/// threshold: Some(2), +/// salt: None, +/// })?; +/// assert_eq!(stacked.id.as_deref(), Some("my-stack")); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn stack(factors: Vec, options: StackOptions) -> MFKDF2Result { let id = match options.id { None => Some("stack".to_string()), @@ -87,8 +128,9 @@ pub fn stack(factors: Vec, options: StackOptions) -> MFKDF2Result< }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_stack( +async fn setup_stack( factors: Vec, options: StackOptions, ) -> MFKDF2Result { diff --git a/mfkdf2/src/setup/factors/totp.rs b/mfkdf2/src/setup/factors/totp.rs index 55281f86..18ad09e8 100644 --- a/mfkdf2/src/setup/factors/totp.rs +++ b/mfkdf2/src/setup/factors/totp.rs @@ -1,3 +1,42 @@ +//! Time-based TOTP factor setup. +//! +//! This factor models an RFC 6238 TOTP "soft token" (for example, Google +//! Authenticator or any compatible app) and the corresponding server-side logic +//! used by MFKDF2 to turn a changing time-based code into stable factor +//! material. +//! +//! Conceptually: +//! - a TOTP app holds a shared key hotkeyₜ and derives one-time codes otpₜ,ᵢ at regular time steps +//! using a counter derived from Unix time; +//! - MFKDF2 needs a fixed secret σₜ instead of a different code each step, so a random targetₜ is +//! chosen in the range [0, 10ᵈ) (where `d` is the number of digits) and express each observed +//! otpₜ,ᵢ as "targetₜ plus an offset" modulo 10ᵈ. +//! +//! Let T₀ be the starting Unix time, X the TOTP step/period in seconds, and T +//! the current Unix time. TOTP is essentially HOTP with a time-based counter: +//! TOTP(K) = HOTP(K, ⌊(T − T₀) / X⌋). During setup this module: +//! - fixes an initial time T₀ (stored as `start`) and a window size `w` of future steps for which +//! offsets will be precomputed; +//! - draws a random targetₜ ∈ [0, 10ᵈ); +//! - for each counter value in {ctr, ctr + 1, …, ctr + w − 1} corresponding to that window, +//! computes the TOTP code otpₜ,ᵢ using hotkeyₜ and stores an offset offsetₜ,ᵢ = (targetₜ − +//! otpₜ,ᵢ) % 10ᵈ; +//! - encrypts the padded TOTP secret under the final derived key K and exposes it as the `pad` +//! field, alongside the packed offsets table, in the public params. +//! +//! The resulting TOTP parameters capture start time, step, window, encrypted secret and precomputed +//! offsets, and are embedded into the MFKDF2 policy. On derive, as long as the current time falls +//! within the precomputed window, the library can reconstruct the same targetₜ from an app-provided +//! otpₜ,ᵢ using the stored offset without talking to the TOTP app. All offset calculation happens +//! inside the library; the authenticator app simply shows otpₜ,ᵢ once per login as usual, and +//! remains unchanged by the presence of MFKDF2. +//! +//! Software-token based key-derivation constructions require no changes to existing authenticator +//! applications like Google Authenticator. Because the HOTP key hotkeyₜ is stored inside the factor +//! state βₜ (encrypted as the pad), the computation of new offset values happens entirely inside +//! the library's setup/derive machinery. The authenticator app is only ever asked to display otpₜ,ᵢ +//! once per login, exactly as it does today; it does not participate directly in the key-derivation +//! logic. use std::collections::HashMap; #[cfg(not(target_arch = "wasm32"))] use std::time::{SystemTime, UNIX_EPOCH}; @@ -10,24 +49,39 @@ use web_time::{SystemTime, UNIX_EPOCH}; use crate::{ crypto::encrypt, - definitions::{FactorMetadata, FactorType, Key, MFKDF2Factor}, + definitions::{FactorType, Key, MFKDF2Factor, factor::FactorMetadata}, error::{MFKDF2Error, MFKDF2Result}, - otpauth::{self, HashAlgorithm, OtpauthUrlOptions, generate_hotp_code}, + otpauth::{self, HashAlgorithm, OtpAuthUrlOptions, generate_otp_token}, setup::{FactorSetup, factors::hotp::mod_positive}, }; +/// Options for configuring a TOTP factor setup #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TOTPOptions { + /// Optional application-defined identifier for the factor. Defaults to `"totp"`. If + /// provided, it must be non-empty pub id: Option, + /// 20‑byte TOTP secret. If omitted, a random secret is generated pub secret: Option>, + /// Number of digits in the OTP code (6–8). Values outside this range cause + /// [`MFKDF2Error::InvalidTOTPDigits`] pub digits: Option, + /// Hash algorithm used by the TOTP generator (default: SHA‑1) pub hash: Option, + /// A string value indicating the provider or service the credential is associated with pub issuer: Option, + /// A string value identifying which account a credential is associated with. It also serves + /// as the unique identifier for the credential itself pub label: Option, - pub time: Option, // Unix epoch time in milliseconds + /// Starting Unix time in milliseconds used to anchor the TOTP window + pub time: Option, + /// Number of TOTP steps for which offsets are precomputed (default is sized for long‑lived + /// offline use) pub window: Option, + /// Step size in seconds (the TOTP "period", default 30s) pub step: Option, + /// Optional per‑time overrides for debugging or advanced flows pub oracle: Option>, } @@ -48,18 +102,33 @@ impl Default for TOTPOptions { } } +/// TOTP configuration #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TOTPConfig { + /// Optional application-defined identifier for the factor. Defaults to `"totp"`. If provided, it + /// must be non-empty pub id: String, + /// 20‑byte TOTP secret. If omitted, a random secret is generated pub secret: Vec, + /// Number of digits in the OTP code (6–8). Values outside this range cause + /// [`MFKDF2Error::InvalidTOTPDigits`] pub digits: u32, + /// Hash algorithm used by the TOTP generator (default: SHA‑1) pub hash: HashAlgorithm, + /// A string value indicating the provider or service the credential is associated with pub issuer: String, + /// A string value identifying which account a credential is associated with. It also serves + /// as the unique identifier for the credential itself pub label: String, + /// Starting Unix time in milliseconds used to anchor the TOTP window pub time: u64, + /// Number of TOTP steps for which offsets are precomputed (default is sized for long‑lived + /// offline use) pub window: u32, + /// Step size in seconds (the TOTP "period", default 30s) pub step: u32, + /// Optional timing oracle to harden TOTP factor construction pub oracle: Option>, } @@ -101,24 +170,38 @@ impl Default for TOTPConfig { } } +/// TOTP public parameters. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TOTPParams { + /// Starting Unix time in milliseconds used to anchor the TOTP window pub start: u64, + /// Hash algorithm used by the TOTP generator pub hash: HashAlgorithm, + /// Number of digits in the OTP code pub digits: u32, + /// Step size in seconds (the TOTP "period") pub step: u32, + /// Number of TOTP steps for which offsets are precomputed pub window: u32, + /// Base64 encoded pad pub pad: String, + /// Base64 encoded offsets table + /// The offsets table is a sequence of 4-byte integers, one for each time window slot pub offsets: String, } +/// TOTP factor state #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TOTP { + /// TOTP configuration pub config: TOTPConfig, + /// TOTP public parameters pub params: Value, + /// TOTP code pub code: u32, + /// TOTP factor material. The target code that is used to derive the key pub target: u32, } @@ -142,7 +225,7 @@ impl FactorSetup for TOTP { // Here, T0 is 0 (Unix epoch) and X is self.config.step. // We add 'i' to generate a window of future OTPs for offline use. let counter = (time / 1000) as u64 / u64::from(self.config.step) + i as u64; - let code = generate_hotp_code( + let code = generate_otp_token( &self.config.secret[..20], counter, &self.config.hash, @@ -191,7 +274,7 @@ impl FactorSetup for TOTP { "algorithm": self.config.hash.to_string(), "digits": self.config.digits, "period": self.config.step, - "uri": otpauth::otpauth_url(&OtpauthUrlOptions { + "uri": otpauth::otpauth_url(&OtpAuthUrlOptions { secret: hex::encode(&self.config.secret[..20]), label: self.config.label.clone(), kind: Some(otpauth::Kind::Totp), @@ -199,15 +282,55 @@ impl FactorSetup for TOTP { issuer: Some(self.config.issuer.clone()), digits: Some(self.config.digits), period: Some(self.config.step), - shared: Some(otpauth::SharedOptions { - encoding: Some(otpauth::Encoding::Hex), - algorithm: Some(self.config.hash.clone()), - }), + encoding: Some(otpauth::Encoding::Hex), + algorithm: Some(self.config.hash.clone()), }).unwrap() }) } } +/// Initializes a TOTP factor from the given options. +/// +/// Validates the configuration, generates a random target code and (optionally) secret, and returns +/// an [`MFKDF2Factor`] that can be used in MFKDF2 key setup. The factor's `output()` method exposes +/// an `otpauth://` URI suitable for QR codes, while `params()` returns encrypted secret material +/// and a precomputed offset table for each time window slot. +/// +/// # Errors +/// - [`MFKDF2Error::MissingFactorId`] if `id` is provided but empty +/// - [`MFKDF2Error::InvalidTOTPDigits`] if `digits` is set outside `6..=8` +/// - [`MFKDF2Error::InvalidSecretLength`] if `secret` is provided but not exactly 20 bytes +/// - [`MFKDF2Error::MissingSetupParams`] if required fields like `secret` or `time` are missing +/// when converting to [`TOTPConfig`] +/// +/// # Example +/// +/// ```rust +/// # use mfkdf2::setup::factors::totp::{totp, TOTPOptions}; +/// # use mfkdf2::otpauth::HashAlgorithm; +/// let options = TOTPOptions { +/// id: Some("login-totp".into()), +/// secret: Some(b"shared-totp-secret!!".to_vec()), // 20 bytes +/// digits: Some(6), +/// ..Default::default() +/// }; +/// let factor = totp(options)?; +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Invalid secret length +/// +/// ```rust +/// # use mfkdf2::setup::factors::totp::{totp, TOTPOptions}; +/// # use mfkdf2::error::MFKDF2Error; +/// let options = TOTPOptions { +/// secret: Some(b"my-secret-is-super-secret-123456".to_vec()), +/// ..Default::default() +/// }; +/// let result = totp(options); +/// assert!(matches!(result, Err(MFKDF2Error::InvalidSecretLength(_)))); +/// # Ok::<(), MFKDF2Error>(()) +/// ``` pub fn totp(options: TOTPOptions) -> MFKDF2Result { let mut options = options; @@ -267,8 +390,9 @@ pub fn totp(options: TOTPOptions) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_totp(options: TOTPOptions) -> MFKDF2Result { totp(options) } +async fn setup_totp(options: TOTPOptions) -> MFKDF2Result { totp(options) } #[cfg(test)] mod tests { @@ -306,7 +430,6 @@ mod tests { let factor = result.unwrap(); assert_eq!(factor.id, Some("test".to_string())); - // assert_eq!(factor.salt.len(), 32); assert!(matches!(factor.factor_type, FactorType::TOTP(_))); if let FactorType::TOTP(totp_factor) = factor.factor_type { diff --git a/mfkdf2/src/setup/factors/uuid.rs b/mfkdf2/src/setup/factors/uuid.rs index 0eb41d4a..e67d69d5 100644 --- a/mfkdf2/src/setup/factors/uuid.rs +++ b/mfkdf2/src/setup/factors/uuid.rs @@ -1,17 +1,26 @@ +//! UUID factor setup. +//! +//! This factor uses a random (or caller‑provided) UUID as its secret material. It is useful for +//! device binding or opaque identifiers where you want stable, high‑entropy bytes that are not +//! intended to be memorized by a user. use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; pub use uuid::Uuid; use crate::{ - definitions::{FactorMetadata, FactorType, MFKDF2Factor}, + definitions::{FactorType, MFKDF2Factor, factor::FactorMetadata}, error::MFKDF2Result, setup::FactorSetup, }; +/// Options for configuring a [`UUIDFactor`]. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UUIDOptions { + /// Optional application-defined identifier for the factor. Defaults to `"uuid"`. If + /// provided, it must be non-empty. pub id: Option, + /// Optional pre‑existing UUID. If omitted, a new random UUID v4 is generated during setup. pub uuid: Option, } @@ -19,9 +28,11 @@ impl Default for UUIDOptions { fn default() -> Self { Self { id: Some("uuid".to_string()), uuid: None } } } +/// UUID‑backed factor state. #[cfg_attr(feature = "bindings", derive(uniffi::Record))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UUIDFactor { + /// UUID used as factor material. pub uuid: Uuid, } @@ -42,6 +53,24 @@ impl FactorSetup for UUIDFactor { } } +/// Creates a UUID factor from the given options. +/// +/// If no UUID is supplied, a new v4 UUID is generated. The resulting factor +/// provides ~122 bits of entropy and can be used as a non‑interactive device +/// or account binding factor. +/// +/// # Errors +/// - [`MFKDF2Error::MissingFactorId`](`crate::error::MFKDF2Error::MissingFactorId`) if `id` is +/// provided but empty. +/// +/// # Example +/// +/// ```rust +/// # use mfkdf2::setup::factors::uuid::{uuid, UUIDOptions}; +/// let factor = uuid(UUIDOptions::default())?; +/// assert_eq!(factor.id.as_deref(), Some("uuid")); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn uuid(options: UUIDOptions) -> MFKDF2Result { // Validation if let Some(ref id) = options.id @@ -59,8 +88,9 @@ pub fn uuid(options: UUIDOptions) -> MFKDF2Result { }) } +#[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export)] -pub async fn setup_uuid(options: UUIDOptions) -> MFKDF2Result { uuid(options) } +async fn setup_uuid(options: UUIDOptions) -> MFKDF2Result { uuid(options) } #[cfg(test)] mod tests { diff --git a/mfkdf2/src/setup/key.rs b/mfkdf2/src/setup/key.rs index 683f3f92..aee9e617 100644 --- a/mfkdf2/src/setup/key.rs +++ b/mfkdf2/src/setup/key.rs @@ -1,70 +1,184 @@ +//! The core MFKDF2 algorithm serves as a foundational primitive for deriving a high-entropy +//! master key from a multi-factor policy. Key Setup phase initializes the policy and generates +//! the necessary shares for each factor. +//! +//! 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`]. // TODO (autoparallel): If we use `no-std`, then this use of `HashSet` will need to be // replaced. use std::collections::{HashMap, HashSet}; use argon2::{Argon2, Params, Version}; use base64::{Engine, engine::general_purpose}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; use ssskit::{SecretSharing, Share}; use uuid::Uuid; use crate::{ constants::SECRET_SHARING_POLY, crypto::{encrypt, hkdf_sha256_with_info, hmacsha256}, - definitions::{MFKDF2DerivedKey, MFKDF2Factor, Salt}, + definitions::{MFKDF2DerivedKey, MFKDF2Factor, MFKDF2Options, Salt}, error::{MFKDF2Error, MFKDF2Result}, - policy::Policy, + policy::{Policy, PolicyFactor}, setup::FactorSetup, }; -// TODO (autoparallel): We probably can just use the MFKDF2Factor struct directly here. -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] -pub struct PolicyFactor { - pub id: String, - #[serde(rename = "type")] - pub kind: String, - pub pad: String, - pub salt: String, - pub secret: String, - // TODO (@lonerapier): convert it into a factor based enum - pub params: Value, - #[serde(skip_serializing_if = "Option::is_none")] - pub hint: Option, -} - -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] -#[derive(Clone, Serialize, Deserialize)] -pub struct MFKDF2Options { - pub id: Option, - pub threshold: Option, - pub salt: Option, - pub stack: Option, - pub integrity: Option, - pub time: Option, - pub memory: Option, -} - -impl Default for MFKDF2Options { - fn default() -> Self { - let mut salt = [0u8; 32]; - crate::rng::fill_bytes(&mut salt); - - Self { - id: Some(uuid::Uuid::new_v4().to_string()), - threshold: None, - salt: Some(salt.into()), - stack: None, - integrity: Some(true), - time: Some(0), - memory: Some(0), - } - } -} - +/// Initializes a derived key from a list of factors and options. +/// +/// This function implements the initial `KeySetup` phase of the MFKDF2 protocol, treating the +/// provided `factors` as Witnesses Wᵢ and using [`MFKDF2Options`] to control the policy identifier, +/// Shamir threshold, key‑derivation cost parameters, and integrity checks. +/// +/// Internally, key setup phase samples a master secret `M`, derives a key‑encryption key (KEK) +/// using either Argon2id or a stack‑key HKDF, encrypts the policy key, splits the secret into +/// Shamir shares over the configured polynomial, and attaches per‑factor helper data and entropy +/// estimates to the resulting [`Policy`]. +/// +/// # Arguments +/// +/// * `factors`: Slice of [`MFKDF2Factor`] setup instances that define the multi‑factor access +/// structure; each factor must contains suitable secret material for the factor type +/// * `options`: [`MFKDF2Options`] controlling policy metadata, threshold, salt, stack mode, +/// integrity checks, and key‑derivation cost parameters +/// +/// # Returns +/// +/// On success, returns a [`MFKDF2DerivedKey`] containing: +/// +/// * A [`Policy`] with encoded factors, threshold, salt, and integrity HMAC +/// * A 32‑byte static key `K` +/// * Shamir shares +/// * Per‑factor helper data and entropy statistics capturing the minimum entropy across admissible +/// factor subsets +/// +/// # Examples +/// +/// Basic password‑only setup using default options, where the threshold implicitly equals the +/// number of factors (n‑of‑n) +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::{MFKDF2Error, MFKDF2Result}, +/// setup::{ +/// self, +/// factors::password::{PasswordOptions, password}, +/// }, +/// }; +/// +/// let factors = vec![password("correct horse battery staple", PasswordOptions::default())?]; +/// let options = MFKDF2Options::default(); +/// +/// let setup_key = setup::key(&factors, options)?; +/// +/// assert_eq!(setup_key.policy.threshold as usize, factors.len()); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Explicit threshold with multiple heterogeneous factors, useful when only a subset of factors +/// is expected to be present at derive time +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::MFKDF2Result, +/// setup::{ +/// self, +/// factors::{ +/// hmacsha1::{HmacSha1Options, hmacsha1}, +/// password::{PasswordOptions, password}, +/// }, +/// }, +/// }; +/// +/// let password_factor = password("password123", PasswordOptions { id: Some("pw".to_string()) })?; +/// let hmac_factor = +/// hmacsha1(HmacSha1Options { id: Some("hmac".to_string()), secret: Some(vec![0u8; 20]) })?; +/// +/// let factors = vec![password_factor, hmac_factor]; +/// let options = MFKDF2Options { threshold: Some(1), ..Default::default() }; +/// +/// let setup_key = setup::key(&factors, options)?; +/// +/// assert_eq!(setup_key.policy.threshold, 1); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// Using a caller‑supplied salt and explicit policy identifier to obtain reproducible policy +/// metadata across environments +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::MFKDF2Result, +/// setup::{ +/// self, +/// factors::password::{PasswordOptions, password}, +/// }, +/// }; +/// +/// let factor = password("password123", PasswordOptions { id: Some("pw".to_string()) })?; +/// let salt = [42u8; 32]; +/// let options = MFKDF2Options { +/// id: Some("my‑policy‑id".to_string()), +/// salt: Some(salt.into()), +/// ..Default::default() +/// }; +/// +/// let setup_key = setup::key(&[factor], options)?; +/// +/// assert_eq!(setup_key.policy.id, "my‑policy‑id"); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// # Errors +/// +/// The function returns +/// [`MFKDF2Error::InvalidThreshold`](`crate::error::MFKDF2Error::InvalidThreshold`) when the +/// requested threshold is outside the closed interval [1, n], where n is the number of provided +/// factors; this includes the case where `factors` is empty +/// +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::{MFKDF2Error, MFKDF2Result}, +/// setup::{ +/// self, +/// factors::password::{PasswordOptions, password}, +/// }, +/// }; +/// let factors = Vec::new(); +/// let options = MFKDF2Options { threshold: Some(0), ..Default::default() }; +/// +/// let setup_key = setup::key(&factors, options); +/// assert!(matches!(setup_key, Err(MFKDF2Error::InvalidThreshold))); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` +/// +/// The function returns +/// [`MFKDF2Error::DuplicateFactorId`](`crate::error::MFKDF2Error::DuplicateFactorId`) when two or +/// more factors share the same identifier, causing the policy factor set to violate the uniqueness +/// constraint on ids +/// ```rust +/// use mfkdf2::{ +/// definitions::MFKDF2Options, +/// error::MFKDF2Error, +/// setup, +/// setup::factors::password::{PasswordOptions, password}, +/// }; +/// +/// let f1 = password("pw1", PasswordOptions { id: Some("dup".to_string()) })?; +/// let f2 = password("pw2", PasswordOptions { id: Some("dup".to_string()) })?; +/// let factors = vec![f1, f2]; +/// let options = MFKDF2Options::default(); +/// +/// let setup_key = setup::key(&factors, options); +/// assert!(matches!(setup_key, Err(MFKDF2Error::DuplicateFactorId))); +/// # Ok::<(), mfkdf2::error::MFKDF2Error>(()) +/// ``` pub fn key(factors: &[MFKDF2Factor], options: MFKDF2Options) -> MFKDF2Result { - assert!(factors.len() < 256, "MFKDF2 supports at most 255 factors"); + if factors.len() > 255 { + return Err(MFKDF2Error::TooManyFactors); + } // Sets the threshold to be the number of factors (n of n) if not provided. let threshold = options.threshold.unwrap_or(factors.len() as u8); @@ -227,8 +341,9 @@ pub fn key(factors: &[MFKDF2Factor], options: MFKDF2Options) -> MFKDF2Result MFKDF2Result { diff --git a/mfkdf2/src/setup/mod.rs b/mfkdf2/src/setup/mod.rs index b40729c8..6a004d47 100644 --- a/mfkdf2/src/setup/mod.rs +++ b/mfkdf2/src/setup/mod.rs @@ -1,18 +1,27 @@ +//! # MFKDF2 Key Setup +//! An MFKDF Key Instance Kᵢ is a tuple (βᵢ, key) representing the i-th derivation of the key. +//! Initial derivation of the key is performed by [`KeySetup`](`crate::setup::key::key`) that takes +//! [factor instances](`crate::definitions::factor::MFKDF2Factor`) and produces the +//! [`MFKDF2DerivedKey`](`crate::definitions::MFKDF2DerivedKey`). pub mod factors; -pub mod key; +mod key; pub use key::key; -use serde::{Serialize, de::DeserializeOwned}; +use serde::{Deserialize, Serialize}; use crate::{definitions::Key, error::MFKDF2Result}; -#[allow(unused_variables)] -pub trait FactorSetup: Send + Sync + std::fmt::Debug { - type Params: Serialize + DeserializeOwned + std::fmt::Debug + Default; - type Output: Serialize + DeserializeOwned + std::fmt::Debug + Default; +/// Trait for factor setup. +pub(crate) trait FactorSetup { + /// Public parameters for the factor setup. + type Params: Serialize + for<'de> Deserialize<'de> + Default; + /// Public output for the factor setup. + type Output: Serialize + for<'de> Deserialize<'de> + Default; - fn params(&self, key: Key) -> MFKDF2Result { + /// Returns the public parameters for the factor setup. + fn params(&self, _key: Key) -> MFKDF2Result { Ok(serde_json::from_value(serde_json::json!({}))?) } + /// Returns the public output for the factor setup. fn output(&self) -> Self::Output { serde_json::from_value(serde_json::json!({})).unwrap() } } diff --git a/mfkdf2/tests/common/mod.rs b/mfkdf2/tests/common/mod.rs index 5a53e417..8e17cc72 100644 --- a/mfkdf2/tests/common/mod.rs +++ b/mfkdf2/tests/common/mod.rs @@ -1,12 +1,16 @@ #![allow(dead_code)] use base64::Engine; -use mfkdf2::definitions::MFKDF2DerivedKey; +use hkdf::Hkdf; +use hmac::{Hmac, Mac}; +use mfkdf2::definitions::{MFKDF2DerivedKey, MFKDF2Options}; use rsa::{ RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, traits::PublicKeyParts, }; +use sha1::Sha1; +use sha2::Sha256; pub const HMACSHA1_SECRET: [u8; 20] = [ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, @@ -18,6 +22,13 @@ pub const PASSKEY_SECRET: [u8; 32] = [7; 32]; pub const RSA_PUBLIC_KEY: &str = r#"3082010a0282010100bc2275550f8c1a80bad27476c18cb4e2906c686468cccd42318bede7bbdbaf3c0da4d799586ee1ceebe30c08bb1a9dbd09bad68faaa4082e2d9f76a295caf4571f96058fdc0e3ccaa88cd6037fb7e74a6afae9fff3c43f4a0c8e9f8f22294df6d77969744092c54a15039e6d71862ac9702205afbff48b3b3f7046cfbff2938f202d00060979a6c1822c309ef7bd008812f729d2b5c6f3d79f4b316e712811aebade3320b1f28901acab225c64c51ad0c4c1dc93a254a636afddaf2c6a9e8d8114fb24f7a716c58030e9bae040e044a1f47d921c0a3e157389da1891f1b72a9e27896ecc4981a09eafa0a71f00a7c72b1fc7c98659eaf9576dc295aaaf0866c10203010001"#; pub const RSA_PRIVATE_KEY: &str = r#"308204a40201000282010100bc2275550f8c1a80bad27476c18cb4e2906c686468cccd42318bede7bbdbaf3c0da4d799586ee1ceebe30c08bb1a9dbd09bad68faaa4082e2d9f76a295caf4571f96058fdc0e3ccaa88cd6037fb7e74a6afae9fff3c43f4a0c8e9f8f22294df6d77969744092c54a15039e6d71862ac9702205afbff48b3b3f7046cfbff2938f202d00060979a6c1822c309ef7bd008812f729d2b5c6f3d79f4b316e712811aebade3320b1f28901acab225c64c51ad0c4c1dc93a254a636afddaf2c6a9e8d8114fb24f7a716c58030e9bae040e044a1f47d921c0a3e157389da1891f1b72a9e27896ecc4981a09eafa0a71f00a7c72b1fc7c98659eaf9576dc295aaaf0866c10203010001028201005191e32b893d26b48fcbf5e113942d4d5a6f16680aa4598e8caaedf09e9be683742af7abae130d66c911bd42ffd4cf758a056f4805256fc28dd768f99f56cad0078ae5487591dabbc78ea0b00dad2fe42d343346dd6b464195e634ba3b868b1e2e589ee75fa40354567e262faf9c0b6b216a2eeaffa048c9dc7c92c73aa33364898898cd981df9929c6ce50b17effb7e27909e8d775c290639a625dce01e3a6f1cda2a55de1c0ace5e8eaf63e416243c12cbc68fc09554f7a3b7bb92ec1fe5a426de06b855004f61e1f84029430b7c04794d1cf8b480a274b9e8db1288203563b8547fe9141637ac3c8246b0847b7aa942e8d55bb9a11fc38924df403fadf03d02818100e4a543f316bbdc75c56d24f4a7ee6c4713e998d56eeda52402e42fb63cb1324ab856be40b16c3a903a86a7b78003c51e84e53af9d3b7e4b2da44b8482b33eec3e3d98e1cf29d07a496442d2e0f0712b276eb8420e41d625d4641379dbb4d7ac2ece58b72d12810155ff1abc4597ba9e6f2a02c925a0fd1b0bce62f16bc977fd702818100d2a4765dfcd3a1b1cdd07bc01b00aac6a62d93502096f0f711142b9cda50eb9cfdc50ae8324f9c8f2eb06e30232e37f45e5e6849a8a7c05811e4c1db5718390f30b278d2b06effc26241911b222caa78bea60b39d0440b241023c1f5588f7e93744c42dec35bdd383c156837357297cdf36c1044788990196ceafe03bc41db2702818100ba2354db0c51e9db32db74ef7bdb1ce90c6bea912f1a668b9792fec8a44639441d27f9009fb015491f6c4a139832f981abfd15f3168a29b3f4ff66ead1c91882fef638bc9642825b5a3dac6e47aba16c0a66178dd3479cb184a5494aae9617efa27e08f57312e36d134ba26359d9d3ea80f126f80a3bc0a0da57a654233a4ec7028180246651220ab79380833d5cb524b567cd6e180015df9bd5c60c087d44dca111260ee046f33b0670da7949f9b08dd3c5cd8fa526c65bc3a9444ecb46089e334c60e89c5eaea1d87c8fdda4d0eb6c6b6585fa03fd7a9f17b3092754d6868c2837ca49558854b053a695ba2444df0d7860ed30fc628f42791b1299b4bdf26d4cc00f02818100e0b13a82d6941af546f0b9838c384cc3c121bd12f6313ac5605d7b77cf5b651239eb3d90316999619cbedcc84014a447104082134de086fce9a9cc813c3dfe2b47a1b424dd646890909ad7a987a8577e5256892dc1d186ad20971223cac0881349fc4fe4cfbd6421a49fa5ec1abe52f6faad264f8d93c65be84c753287241fc7"#; +/// Derives a 32-byte key using HKDF-SHA256 with the given salt and info. +pub fn hkdf_sha256_with_info(input: &[u8], salt: &[u8], info: &[u8]) -> [u8; 32] { + let mut okm = [0u8; 32]; + Hkdf::::new(Some(salt), input).expand(info, &mut okm).unwrap(); + okm +} + fn keypair() -> (RsaPrivateKey, RsaPublicKey) { let bits = 2048; let private_key = @@ -48,7 +59,7 @@ pub fn mock_mfkdf2_password() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -67,7 +78,7 @@ pub fn mock_threshold_mfkdf2() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options { threshold: Some(1), ..Default::default() }; + let options = MFKDF2Options { threshold: Some(1), ..Default::default() }; let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -87,7 +98,7 @@ pub fn mock_password_question_mfkdf2() .into_iter() .collect::, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -99,7 +110,7 @@ pub fn mock_uuid_mfkdf2() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -113,7 +124,7 @@ pub fn mock_hmacsha1_mfkdf2() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -127,7 +138,7 @@ pub fn mock_hotp_mfkdf2() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -147,7 +158,7 @@ pub fn mock_mixed_factors_mfkdf2() -> Result, _>>()?; - let options = mfkdf2::setup::key::MFKDF2Options::default(); + let options = MFKDF2Options::default(); let key = mfkdf2::setup::key(&factors, options)?; Ok(key) } @@ -194,7 +205,7 @@ pub fn create_setup_factor(name: &str) -> mfkdf2::definitions::MFKDF2Factor { let rsa_public_key = RsaPublicKey::from_pkcs1_der(&hex::decode(RSA_PUBLIC_KEY).unwrap()).unwrap(); let test_jwk = jwk(&rsa_public_key); - mfkdf2::setup::factors::ooba::ooba(mfkdf2::setup::factors::ooba::OobaOptions { + mfkdf2::setup::factors::ooba(mfkdf2::setup::factors::ooba::OobaOptions { id: Some("ooba_1".to_string()), length: Some(8), key: Some(test_jwk), @@ -225,7 +236,7 @@ pub fn create_derive_factor( let hash = serde_json::from_value(params["hash"].clone()).unwrap(); let generated_code = - mfkdf2::otpauth::generate_hotp_code(&HOTP_SECRET, counter, &hash, digits); + mfkdf2::otpauth::generate_otp_token(&HOTP_SECRET, counter, &hash, digits); ("hotp_1".to_string(), mfkdf2::derive::factors::hotp(generated_code).unwrap()) }, "totp" => { @@ -238,14 +249,19 @@ pub fn create_derive_factor( let digits = params["digits"].as_u64().unwrap() as u32; let counter = time as u64 / (step * 1000); - let totp_code = mfkdf2::otpauth::generate_hotp_code(&TOTP_SECRET, counter, &hash, digits); + let totp_code = mfkdf2::otpauth::generate_otp_token(&TOTP_SECRET, counter, &hash, digits); ("totp_1".to_string(), mfkdf2::derive::factors::totp(totp_code, None).unwrap()) }, "hmacsha1" => { let factor_policy = policy.factors.iter().find(|f| f.id == "hmacsha1_1").unwrap(); let params = &factor_policy.params; let challenge = hex::decode(params["challenge"].as_str().unwrap()).unwrap(); - let response = mfkdf2::crypto::hmacsha1(&HMACSHA1_SECRET, &challenge); + let response: [u8; 20] = as Mac>::new_from_slice(&HMACSHA1_SECRET) + .unwrap() + .chain_update(challenge) + .finalize() + .into_bytes() + .into(); ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response.into()).unwrap()) }, "question" => @@ -273,7 +289,7 @@ pub fn create_derive_factor( ("ooba_1".to_string(), mfkdf2::derive::factors::ooba(code).unwrap()) }, "passkey" => - ("passkey_1".to_string(), mfkdf2::derive::factors::passkey::passkey(PASSKEY_SECRET).unwrap()), + ("passkey_1".to_string(), mfkdf2::derive::factors::passkey(PASSKEY_SECRET).unwrap()), _ => panic!("Unknown factor type for derive: {}", name), } } diff --git a/mfkdf2/tests/entropy.rs b/mfkdf2/tests/entropy.rs index ffe2e53d..10bad0e9 100644 --- a/mfkdf2/tests/entropy.rs +++ b/mfkdf2/tests/entropy.rs @@ -1,6 +1,5 @@ use mfkdf2::{ - policy::setup::PolicySetupOptions, - setup::{factors::password::PasswordOptions, key::MFKDF2Options}, + definitions::MFKDF2Options, policy::PolicySetupOptions, setup::factors::password::PasswordOptions, }; #[test] @@ -79,16 +78,16 @@ fn entropy_1_of_3_passwords() -> Result<(), mfkdf2::error::MFKDF2Error> { #[test] fn entropy_policy_combinators() -> Result<(), mfkdf2::error::MFKDF2Error> { // Mirrors the complex AND/OR/ANY nesting from the JS test - let policy = mfkdf2::policy::setup::setup( - mfkdf2::policy::logic::and( + let policy = mfkdf2::policy::setup( + mfkdf2::policy::and( mfkdf2::setup::factors::password("12345678", PasswordOptions { id: Some("password1".to_string()), })?, - mfkdf2::policy::logic::any(vec![ + mfkdf2::policy::any(vec![ mfkdf2::setup::factors::password("12345678", PasswordOptions { id: Some("password7".to_string()), })?, - mfkdf2::policy::logic::or( + mfkdf2::policy::or( mfkdf2::setup::factors::password("12345678", PasswordOptions { id: Some("password3".to_string()), })?, @@ -96,11 +95,11 @@ fn entropy_policy_combinators() -> Result<(), mfkdf2::error::MFKDF2Error> { id: Some("password2".to_string()), })?, )?, - mfkdf2::policy::logic::and( + mfkdf2::policy::and( mfkdf2::setup::factors::password("12345678", PasswordOptions { id: Some("password4".to_string()), })?, - mfkdf2::policy::logic::or( + mfkdf2::policy::or( mfkdf2::setup::factors::password("12345678", PasswordOptions { id: Some("password5".to_string()), })?, diff --git a/mfkdf2/tests/integration.rs b/mfkdf2/tests/integration.rs index 78930bef..c8751660 100644 --- a/mfkdf2/tests/integration.rs +++ b/mfkdf2/tests/integration.rs @@ -2,7 +2,9 @@ mod common; use std::collections::HashMap; +use hmac::{Hmac, Mac}; use rstest::rstest; +use sha1::Sha1; use crate::common::*; @@ -130,7 +132,12 @@ fn key_derive_hmacsha1() -> Result<(), mfkdf2::error::MFKDF2Error> { ) .unwrap(); - let response = mfkdf2::crypto::hmacsha1(&HMACSHA1_SECRET, &challenge); + let response: [u8; 20] = as Mac>::new_from_slice(&HMACSHA1_SECRET) + .unwrap() + .chain_update(challenge) + .finalize() + .into_bytes() + .into(); let factor = ("hmacsha1_1".to_string(), mfkdf2::derive::factors::hmacsha1(response.into()).unwrap()); @@ -202,7 +209,7 @@ fn key_derive_hotp() -> Result<(), mfkdf2::error::MFKDF2Error> { // Generate the HOTP code that the user would need to provide // This simulates what would come from an authenticator app - let generated_code = mfkdf2::otpauth::generate_hotp_code(&HOTP_SECRET, counter, &hash, digits); + let generated_code = mfkdf2::otpauth::generate_otp_token(&HOTP_SECRET, counter, &hash, digits); println!("Generated HOTP code: {}", generated_code); @@ -243,7 +250,7 @@ fn totp_static() -> Result<(), mfkdf2::error::MFKDF2Error> { time: Some(1), ..Default::default() })?], - mfkdf2::setup::key::MFKDF2Options::default(), + mfkdf2::definitions::MFKDF2Options::default(), )?; let derived_key1 = mfkdf2::derive::key( @@ -297,7 +304,7 @@ fn key_derive_mixed_password_hotp() -> Result<(), mfkdf2::error::MFKDF2Error> { let hash = serde_json::from_value(params["hash"].clone()).unwrap(); // Generate the correct HOTP code using SHA256 (different from previous test) - let generated_code = mfkdf2::otpauth::generate_hotp_code(&HOTP_SECRET, counter, &hash, digits); + let generated_code = mfkdf2::otpauth::generate_otp_token(&HOTP_SECRET, counter, &hash, digits); println!("Generated HOTP code (SHA256): {}", generated_code); @@ -332,7 +339,7 @@ fn key_derivation_combinations( let setup_factors: Vec<_> = setup_factor_names.into_iter().map(create_setup_factor).collect(); let options = - mfkdf2::setup::key::MFKDF2Options { threshold: Some(threshold), ..Default::default() }; + mfkdf2::definitions::MFKDF2Options { threshold: Some(threshold), ..Default::default() }; let setup_key = mfkdf2::setup::key(&setup_factors, options)?; // 2. Loop through derivation combinations diff --git a/mfkdf2/tests/integrity.rs b/mfkdf2/tests/integrity.rs index 325e61ef..ea052642 100644 --- a/mfkdf2/tests/integrity.rs +++ b/mfkdf2/tests/integrity.rs @@ -4,18 +4,18 @@ mod common; use std::collections::HashMap; -use mfkdf2::{definitions::MFKDF2DerivedKey, policy::Policy}; +use mfkdf2::{ + definitions::{MFKDF2DerivedKey, MFKDF2Options}, + policy::Policy, +}; use crate::common::{create_derive_factor, create_setup_factor}; fn make_policy(setup_factor_names: &[&str], threshold: u8, integrity: bool) -> MFKDF2DerivedKey { let setup_factors: Vec<_> = setup_factor_names.iter().copied().map(create_setup_factor).collect(); - let options = mfkdf2::setup::key::MFKDF2Options { - threshold: Some(threshold), - integrity: Some(integrity), - ..Default::default() - }; + let options = + MFKDF2Options { threshold: Some(threshold), integrity: Some(integrity), ..Default::default() }; mfkdf2::setup::key(&setup_factors, options).unwrap() } diff --git a/mfkdf2/tests/security.rs b/mfkdf2/tests/security.rs index 92ab52d8..a1421f81 100644 --- a/mfkdf2/tests/security.rs +++ b/mfkdf2/tests/security.rs @@ -1,3 +1,4 @@ +mod common; use std::{ collections::HashMap, time::{SystemTime, UNIX_EPOCH}, @@ -6,18 +7,18 @@ use std::{ use base64::{Engine, engine::general_purpose}; use mfkdf2::{ constants::SECRET_SHARING_POLY, - crypto::hkdf_sha256_with_info, + definitions::MFKDF2Options, derive, error::MFKDF2Error, - otpauth::{HashAlgorithm, generate_hotp_code}, + otpauth::{HashAlgorithm, generate_otp_token}, policy, - policy::setup::PolicySetupOptions, + policy::PolicySetupOptions, setup::{ self, factors::{hotp::HOTPOptions, password::PasswordOptions, totp::TOTPOptions}, - key::MFKDF2Options, }, }; +use rand_chacha::rand_core::{RngCore, SeedableRng}; // Helper function to perform XOR operation on two byte arrays fn xor(a: &[u8], b: &[u8]) -> Vec { a.iter().zip(b.iter()).map(|(x, y)| x ^ y).collect() } @@ -26,20 +27,20 @@ fn xor(a: &[u8], b: &[u8]) -> Vec { a.iter().zip(b.iter()).map(|(x, y)| x ^ fn generate_totp_code(secret: &[u8], step: u64, hash: &HashAlgorithm, digits: u32) -> u32 { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); let counter = now / step; - generate_hotp_code(secret, counter, hash, digits) + generate_otp_token(secret, counter, hash, digits) } #[test] fn factor_fungibility_correct() -> Result<(), MFKDF2Error> { - let setup = policy::setup::setup( - policy::logic::and( + let setup = policy::setup( + policy::and( setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()) })?, setup::factors::password("password2", PasswordOptions { id: Some("password2".to_string()) })?, )?, PolicySetupOptions::default(), )?; - let derive = policy::derive::derive( + let derive = policy::derive( &setup.policy, &HashMap::from([ ("password1".to_string(), derive::factors::password("password1")?), @@ -55,8 +56,8 @@ fn factor_fungibility_correct() -> Result<(), MFKDF2Error> { #[test] fn factor_fungibility_incorrect() { - let setup = policy::setup::setup( - policy::logic::and( + let setup = policy::setup( + policy::and( setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()) }) .unwrap(), setup::factors::password("password2", PasswordOptions { id: Some("password2".to_string()) }) @@ -67,7 +68,7 @@ fn factor_fungibility_incorrect() { ) .unwrap(); - let derive = policy::derive::derive( + let derive = policy::derive( &setup.policy, &HashMap::from([ ("password1".to_string(), derive::factors::password("password2").unwrap()), @@ -82,11 +83,13 @@ fn factor_fungibility_incorrect() { #[test] fn share_indistinguishability_share_size() -> Result<(), MFKDF2Error> { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([10u8; 32]); + let mut secret = [0u8; 32]; - mfkdf2::rng::fill_bytes(&mut secret); + rng.fill_bytes(&mut secret); let sss = ssskit::SecretSharing::(1); - let shares1 = sss.dealer_rng(&secret, &mut mfkdf2::rng::GlobalRng).take(3).collect::>(); + let shares1 = sss.dealer_rng(&secret, &mut rng).take(3).collect::>(); assert_eq!(shares1.len(), 3); for share in shares1.iter() { assert_eq!(share.y.len(), 32); @@ -99,7 +102,7 @@ fn share_indistinguishability_share_size() -> Result<(), MFKDF2Error> { assert_eq!(combined2, secret); let sss = ssskit::SecretSharing::(2); - let shares2 = sss.dealer_rng(&secret, &mut mfkdf2::rng::GlobalRng).take(3).collect::>(); + let shares2 = sss.dealer_rng(&secret, &mut rng).take(3).collect::>(); assert_eq!(shares2.len(), 3); for share in shares2.iter() { assert_eq!(share.y.len(), 32); @@ -112,7 +115,7 @@ fn share_indistinguishability_share_size() -> Result<(), MFKDF2Error> { assert_eq!(combined4, secret); let sss = ssskit::SecretSharing::(3); - let shares3 = sss.dealer_rng(&secret, &mut mfkdf2::rng::GlobalRng).take(3).collect::>(); + let shares3 = sss.dealer_rng(&secret, &mut rng).take(3).collect::>(); assert_eq!(shares3.len(), 3); for share in shares3.iter() { assert_eq!(share.y.len(), 32); @@ -128,7 +131,7 @@ fn share_indistinguishability_share_size() -> Result<(), MFKDF2Error> { #[test] fn share_encryption_correct() -> Result<(), MFKDF2Error> { // Setup with two password factors using direct key setup - let setup = setup::key::key( + let setup = setup::key( &[ setup::factors::password("password1", PasswordOptions { id: Some("password1".to_string()) })?, setup::factors::password("password2", PasswordOptions { id: Some("password2".to_string()) })?, @@ -140,11 +143,11 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { let materialp1 = derive::factors::password("password1")?; let padp1 = general_purpose::STANDARD.decode(&setup.policy.factors[0].pad)?; let salt_bytes = general_purpose::STANDARD.decode(&setup.policy.factors[0].salt)?; - let stretchedp1 = hkdf_sha256_with_info(&materialp1.data(), &salt_bytes, &[]); + let stretchedp1 = crate::common::hkdf_sha256_with_info(&materialp1.data(), &salt_bytes, &[]); let sharep1 = xor(&padp1, &stretchedp1); // Derive the key normally - let mut derive = policy::derive::derive( + let mut derive = policy::derive( &setup.policy.clone(), &HashMap::from([ ("password1".to_string(), derive::factors::password("password1")?), @@ -161,7 +164,7 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { })?)?; // Try to derive with old password - should fail - let derive2f = policy::derive::derive( + let derive2f = policy::derive( &derive.policy, &HashMap::from([ ("password1".to_string(), derive::factors::password("password1")?), @@ -172,7 +175,7 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { assert_ne!(derive2f.key, setup.key); // Derive with new password - should succeed - let mut derive2 = policy::derive::derive( + let mut derive2 = policy::derive( &derive.policy, &HashMap::from([ ("password1".to_string(), derive::factors::password("newPassword1")?), @@ -186,7 +189,7 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { let materialp3 = derive::factors::password("newPassword1")?; let padp3 = general_purpose::STANDARD.decode(&derive.policy.factors[0].pad)?; let salt_bytes3 = general_purpose::STANDARD.decode(&derive.policy.factors[0].salt)?; - let stretchedp3 = hkdf_sha256_with_info(&materialp3.data(), &salt_bytes3, &[]); + let stretchedp3 = crate::common::hkdf_sha256_with_info(&materialp3.data(), &salt_bytes3, &[]); let sharep3 = xor(&padp3, &stretchedp3); // Recover factor again with another new password @@ -195,7 +198,7 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { })?)?; // Derive with the second new password - should succeed - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive2.policy, &HashMap::from([ ("password1".to_string(), derive::factors::password("newPassword2")?), @@ -215,7 +218,7 @@ fn share_encryption_correct() -> Result<(), MFKDF2Error> { fn factor_secret_encryption_hotp() -> Result<(), MFKDF2Error> { // Setup HOTP factor with specific secret let secret = b"abcdefghijklmnopqrst".to_vec(); - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::hotp::hotp(HOTPOptions { secret: Some(secret.clone()), ..Default::default() @@ -240,9 +243,9 @@ fn factor_secret_encryption_hotp() -> Result<(), MFKDF2Error> { assert_ne!(recover_hex, key_prefix); // Derive with the correct HOTP code - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, - &HashMap::from([("hotp".to_string(), derive::factors::hotp::hotp(241063)?)]), + &HashMap::from([("hotp".to_string(), derive::factors::hotp(241063)?)]), None, )?; @@ -255,7 +258,7 @@ fn factor_secret_encryption_hotp() -> Result<(), MFKDF2Error> { fn factor_secret_encryption_totp() -> Result<(), MFKDF2Error> { // Setup TOTP factor with specific secret and time let secret = b"abcdefghijklmnopqrst".to_vec(); - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::totp::totp(TOTPOptions { secret: Some(secret.clone()), time: Some(1), @@ -281,11 +284,11 @@ fn factor_secret_encryption_totp() -> Result<(), MFKDF2Error> { assert_ne!(recover_hex, key_prefix); // Derive with the correct TOTP code - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( 953265, Some(derive::factors::totp::TOTPDeriveOptions { time: Some(1), oracle: None }), )?, @@ -301,10 +304,8 @@ fn factor_secret_encryption_totp() -> Result<(), MFKDF2Error> { #[test] fn totp_dynamic_no_oracle() -> Result<(), MFKDF2Error> { // Setup TOTP factor with default options - let setup = setup::key::key( - &[setup::factors::totp::totp(TOTPOptions::default())?], - MFKDF2Options::default(), - )?; + let setup = + setup::key(&[setup::factors::totp::totp(TOTPOptions::default())?], MFKDF2Options::default())?; // Get the secret from the setup outputs let outputs = setup.outputs.get("totp").unwrap(); @@ -325,29 +326,29 @@ fn totp_dynamic_no_oracle() -> Result<(), MFKDF2Error> { let digits = outputs["digits"].as_u64().unwrap() as u32; // Derive multiple times with the same oracle - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy.clone(), &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp(generate_totp_code(&secret, step, &hash, digits), None)?, + derive::factors::totp(generate_totp_code(&secret, step, &hash, digits), None)?, )]), None, )?; - let derive2 = policy::derive::derive( + let derive2 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp(generate_totp_code(&secret, step, &hash, digits), None)?, + derive::factors::totp(generate_totp_code(&secret, step, &hash, digits), None)?, )]), None, )?; - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive2.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp(generate_totp_code(&secret, step, &hash, digits), None)?, + derive::factors::totp(generate_totp_code(&secret, step, &hash, digits), None)?, )]), None, )?; @@ -373,7 +374,7 @@ fn totp_dynamic_valid_fixed_oracle() { } // Setup TOTP factor with oracle - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::totp::totp(TOTPOptions { oracle: Some(oracle.clone()), ..Default::default() @@ -401,11 +402,11 @@ fn totp_dynamic_valid_fixed_oracle() { }; let digits = outputs["digits"].as_u64().unwrap() as u32; - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle.clone()), @@ -418,11 +419,11 @@ fn totp_dynamic_valid_fixed_oracle() { ) .unwrap(); - let derive2 = policy::derive::derive( + let derive2 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle.clone()), @@ -435,11 +436,11 @@ fn totp_dynamic_valid_fixed_oracle() { ) .unwrap(); - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive2.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle.clone()), @@ -470,7 +471,7 @@ fn totp_dynamic_invalid_fixed_oracle() { } // Setup TOTP factor with oracle - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::totp::totp(TOTPOptions { oracle: Some(oracle.clone()), ..Default::default() @@ -507,11 +508,11 @@ fn totp_dynamic_invalid_fixed_oracle() { } // Derive with the different oracle - this should produce different keys - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle2.clone()), @@ -524,11 +525,11 @@ fn totp_dynamic_invalid_fixed_oracle() { ) .unwrap(); - let derive2 = policy::derive::derive( + let derive2 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle2.clone()), @@ -541,11 +542,11 @@ fn totp_dynamic_invalid_fixed_oracle() { ) .unwrap(); - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive2.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle2), @@ -576,7 +577,7 @@ fn totp_dynamic_valid_dynamic_oracle() { } // Setup TOTP factor with oracle - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::totp::totp(TOTPOptions { oracle: Some(oracle.clone()), ..Default::default() @@ -605,11 +606,11 @@ fn totp_dynamic_valid_dynamic_oracle() { let digits = outputs["digits"].as_u64().unwrap() as u32; // Derive multiple times with the same oracle (should succeed) - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle.clone()), @@ -622,11 +623,11 @@ fn totp_dynamic_valid_dynamic_oracle() { ) .unwrap(); - let derive2 = policy::derive::derive( + let derive2 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle.clone()), @@ -639,11 +640,11 @@ fn totp_dynamic_valid_dynamic_oracle() { ) .unwrap(); - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive2.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( generate_totp_code(&secret, step, &hash, digits), Some(derive::factors::totp::TOTPDeriveOptions { oracle: Some(oracle), @@ -685,7 +686,7 @@ fn totp_dynamic_invalid_dynamic_oracle() { } // Setup TOTP factor with specific secret and time - let setup = setup::key::key( + let setup = setup::key( &[setup::factors::totp::totp(TOTPOptions { secret: Some(b"abcdefghijklmnopqrst".to_vec()), time: Some(1650430806597), @@ -698,11 +699,11 @@ fn totp_dynamic_invalid_dynamic_oracle() { .unwrap(); // Derive with different oracle and different times/codes - let derive1 = policy::derive::derive( + let derive1 = policy::derive( &setup.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( 528258, Some(derive::factors::totp::TOTPDeriveOptions { time: Some(1650430943604), @@ -715,11 +716,11 @@ fn totp_dynamic_invalid_dynamic_oracle() { ) .unwrap(); - let derive2 = policy::derive::derive( + let derive2 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( 99922, Some(derive::factors::totp::TOTPDeriveOptions { time: Some(1650430991083), @@ -732,11 +733,11 @@ fn totp_dynamic_invalid_dynamic_oracle() { ) .unwrap(); - let derive3 = policy::derive::derive( + let derive3 = policy::derive( &derive1.policy, &HashMap::from([( "totp".to_string(), - derive::factors::totp::totp( + derive::factors::totp( 398884, Some(derive::factors::totp::TOTPDeriveOptions { time: Some(1650431018392), diff --git a/mfkdf2/tests/stack.rs b/mfkdf2/tests/stack.rs index 4ccc3826..e1c459f8 100644 --- a/mfkdf2/tests/stack.rs +++ b/mfkdf2/tests/stack.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use hmac::{Hmac, Mac}; use mfkdf2::{policy::Policy, setup::factors::hmacsha1::HmacSha1Response}; +use sha1::Sha1; const HMACSHA1_SECRET: [u8; 20] = [ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, @@ -58,7 +60,7 @@ fn mock_setup_stack() -> Result as Mac>::new_from_slice(&HMACSHA1_SECRET) + .unwrap() + .chain_update(challenge) + .finalize() + .into_bytes() + .into(); let derived_key = mfkdf2::derive::key( &derived_key.policy, HashMap::from([(