1+ //! This module implements the factor construction derive phase for the OOBA construction from
2+ //! `crate::setup::factors::ooba`.
3+ //! - During setup, the factor samples a random 32‑byte target, encrypts it under a channel‑specific
4+ //! RSA key, and embeds an initial code and metadata in the policy.
5+ //! - During derive, this module consumes a user‑entered OOBA code Wᵢⱼ, decrypts the target using
6+ //! the stored pad, and prepares the next encrypted payload and code for the following login
17use base64:: { Engine , engine:: general_purpose} ;
28use rsa:: Oaep ;
39use serde_json:: { Value , json} ;
@@ -15,6 +21,8 @@ impl FactorDerive for Ooba {
1521 type Output = Value ;
1622 type Params = Value ;
1723
24+ /// Includes the public parameters for in factor state and decrypts the secret material from
25+ /// public parameters.
1826 fn include_params ( & mut self , params : Self :: Params ) -> MFKDF2Result < ( ) > {
1927 let pad_b64 =
2028 params[ "pad" ] . as_str ( ) . ok_or ( MFKDF2Error :: MissingDeriveParams ( "pad" . to_string ( ) ) ) ?;
@@ -42,6 +50,7 @@ impl FactorDerive for Ooba {
4250 Ok ( ( ) )
4351 }
4452
53+ /// Generates a new OOBA code and encrypts the secret material for the next derivation.
4554 fn params ( & self , _key : Key ) -> MFKDF2Result < Self :: Params > {
4655 let code = generate_alphanumeric_characters ( self . length . into ( ) ) . to_uppercase ( ) ;
4756
@@ -67,6 +76,87 @@ impl FactorDerive for Ooba {
6776 }
6877}
6978
79+ /// Factor construction derive phase for an OOBA factor
80+ ///
81+ /// The `code` should be the alphanumeric value delivered over the out‑of‑band channel (for example,
82+ /// SMS or push notification) that corresponds to the initial OOBA policy parameters created during
83+ /// setup.
84+ ///
85+ /// # Errors
86+ ///
87+ /// - [`MFKDF2Error::InvalidOobaCode`] if `code` is empty
88+ /// - [`MFKDF2Error::MissingDeriveParams`] from [`FactorDerive::include_params`] when required
89+ /// fields such as `"pad"` or `"length"` are absent in the policy parameters
90+ /// - [`MFKDF2Error::InvalidDeriveParams`] from [`FactorDerive::include_params`] when fields such as
91+ /// `"pad"`, `"params"`, or `"key"` are malformed or have the wrong type
92+ ///
93+ /// # Example
94+ ///
95+ /// Single‑factor setup/derive using OOBA within KeySetup/KeyDerive:
96+ ///
97+ /// ```rust
98+ /// # use std::collections::HashMap;
99+ /// # use jsonwebtoken::jwk::Jwk;
100+ /// # use rsa::{RsaPrivateKey, RsaPublicKey, traits::PublicKeyParts};
101+ /// # use serde_json::json;
102+ /// # use mfkdf2::{
103+ /// # error::MFKDF2Result,
104+ /// # setup::{
105+ /// # self,
106+ /// # factors::ooba::{ooba as setup_ooba, OobaOptions},
107+ /// # key::MFKDF2Options,
108+ /// # },
109+ /// # derive,
110+ /// # };
111+ /// # use mfkdf2::derive::factors::ooba as derive_ooba;
112+ /// # use base64::Engine;
113+ /// # use sha2::Sha256;
114+ /// # use rsa::Oaep;
115+ /// #
116+ /// # fn main() -> MFKDF2Result<()> {
117+ /// let bits = 2048;
118+ /// let private_key =
119+ /// RsaPrivateKey::new(&mut rsa::rand_core::OsRng, bits).expect("failed to generate a key");
120+ /// let public_key = RsaPublicKey::from(&private_key);
121+ ///
122+ /// let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be());
123+ /// let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be());
124+ /// let jwk: Jwk = serde_json::from_value(json!({
125+ /// "kty": "RSA",
126+ /// "alg": "RSA-OAEP-256",
127+ /// "n": n,
128+ /// "e": e
129+ /// }))?;
130+ ///
131+ /// let setup_factor = setup_ooba(OobaOptions {
132+ /// id: Some("ooba".into()),
133+ /// length: Some(8),
134+ /// key: Some(jwk),
135+ /// params: Some(json!({"foo": "bar"})),
136+ /// })?;
137+ /// let setup_key = setup::key(&[setup_factor], MFKDF2Options::default())?;
138+ ///
139+ /// // Decrypt the first OOBA payload to recover the user-visible code
140+ /// let policy_factor =
141+ /// setup_key.policy.factors.iter().find(|f| f.id == "ooba").unwrap();
142+ /// let setup_params = &policy_factor.params;
143+ /// let ciphertext = hex::decode(setup_params["next"].as_str().unwrap()).unwrap();
144+ /// let plaintext = private_key.decrypt(Oaep::new::<Sha256>(), &ciphertext).unwrap();
145+ /// let decoded: serde_json::Value = serde_json::from_slice(&plaintext).unwrap();
146+ /// let code = decoded["code"].as_str().unwrap();
147+ ///
148+ /// let derive_factor = derive_ooba(code)?;
149+ /// let derived_key = derive::key(
150+ /// &setup_key.policy,
151+ /// HashMap::from([("ooba".to_string(), derive_factor)]),
152+ /// true,
153+ /// false,
154+ /// )?;
155+ ///
156+ /// assert_eq!(derived_key.key, setup_key.key);
157+ /// # Ok(())
158+ /// # }
159+ /// ```
70160pub fn ooba ( code : & str ) -> MFKDF2Result < MFKDF2Factor > {
71161 if code. is_empty ( ) {
72162 return Err ( MFKDF2Error :: InvalidOobaCode ) ;
0 commit comments