use std::ops::Not;
use passkey_types::{
crypto::hmac_sha256,
ctap2::{
Ctap2Error, StatusCode,
extensions::{
AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs,
AuthenticatorPrfValues, HmacSecretSaltOrOutput,
},
},
rand::random_vec,
};
use crate::Authenticator;
#[derive(Debug)]
pub struct HmacSecretConfig {
credentials: HmacSecretCredentialSupport,
on_make_credential_support: bool,
}
impl HmacSecretConfig {
pub fn new_with_uv_only() -> Self {
Self {
credentials: HmacSecretCredentialSupport::WithUvOnly,
on_make_credential_support: false,
}
}
pub fn new_without_uv() -> Self {
Self {
credentials: HmacSecretCredentialSupport::WithoutUv,
on_make_credential_support: false,
}
}
pub fn enable_on_make_credential(mut self) -> Self {
self.on_make_credential_support = true;
self
}
pub fn hmac_secret_mc(&self) -> bool {
self.on_make_credential_support
}
fn supports_no_uv(&self) -> bool {
self.credentials.without_uv()
}
}
#[derive(Debug)]
pub enum HmacSecretCredentialSupport {
WithUvOnly,
WithoutUv,
}
impl HmacSecretCredentialSupport {
fn without_uv(&self) -> bool {
match self {
HmacSecretCredentialSupport::WithUvOnly => false,
HmacSecretCredentialSupport::WithoutUv => true,
}
}
}
impl<S, U> Authenticator<S, U> {
pub(super) fn make_hmac_secret(
&self,
hmac_secret_request: Option<bool>,
) -> Option<passkey_types::StoredHmacSecret> {
let config = self.extensions.hmac_secret.as_ref()?;
if hmac_secret_request.is_some_and(|b| b).not() {
return None;
}
Some(passkey_types::StoredHmacSecret {
cred_with_uv: random_vec(32),
cred_without_uv: config.credentials.without_uv().then(|| random_vec(32)),
})
}
pub(super) fn make_prf(
&self,
passkey_ext: Option<&passkey_types::StoredHmacSecret>,
request: AuthenticatorPrfInputs,
uv: bool,
) -> Result<Option<AuthenticatorPrfMakeOutputs>, StatusCode> {
let Some(ref config) = self.extensions.hmac_secret else {
return Ok(None);
};
let Some(creds) = passkey_ext else {
return Ok(Some(AuthenticatorPrfMakeOutputs {
enabled: false,
results: None,
}));
};
let results = config
.on_make_credential_support
.then(|| {
request.eval.map(|eval| {
let request = HmacSecretSaltOrOutput::new(eval.first, eval.second);
calculate_hmac_secret(creds, request, config, uv)
})
})
.flatten()
.transpose()?;
Ok(Some(AuthenticatorPrfMakeOutputs {
enabled: true,
results: results.map(|shared_secrets| AuthenticatorPrfValues {
first: shared_secrets.first().try_into().unwrap(),
second: shared_secrets.second().map(|b| b.try_into().unwrap()),
}),
}))
}
pub(super) fn get_prf(
&self,
credential_id: &[u8],
passkey_ext: Option<&passkey_types::StoredHmacSecret>,
salts: AuthenticatorPrfInputs,
uv: bool,
) -> Result<Option<AuthenticatorPrfGetOutputs>, StatusCode> {
let Some(ref config) = self.extensions.hmac_secret else {
return Ok(None);
};
let Some(hmac_creds) = passkey_ext else {
return Ok(None);
};
let Some(request) = select_salts(credential_id, salts) else {
return Ok(None);
};
let results = calculate_hmac_secret(hmac_creds, request, config, uv)?;
Ok(Some(AuthenticatorPrfGetOutputs {
results: AuthenticatorPrfValues {
first: results.first().try_into().unwrap(),
second: results.second().map(|b| b.try_into().unwrap()),
},
}))
}
}
fn calculate_hmac_secret(
hmac_creds: &passkey_types::StoredHmacSecret,
salts: HmacSecretSaltOrOutput,
config: &HmacSecretConfig,
uv: bool,
) -> Result<HmacSecretSaltOrOutput, StatusCode> {
let cred_random = if uv {
&hmac_creds.cred_with_uv
} else {
config
.supports_no_uv()
.then_some(hmac_creds.cred_without_uv.as_ref())
.flatten()
.ok_or(Ctap2Error::UserVerificationBlocked)?
};
let output1 = hmac_sha256(cred_random, salts.first());
let output2 = salts.second().map(|salt2| hmac_sha256(cred_random, salt2));
let result = HmacSecretSaltOrOutput::new(output1, output2);
Ok(result)
}
fn select_salts(
credential_id: &[u8],
request: AuthenticatorPrfInputs,
) -> Option<HmacSecretSaltOrOutput> {
if let Some(eval_by_cred) = request.eval_by_credential {
let eval = eval_by_cred
.into_iter()
.find(|(key, _)| key.as_slice() == credential_id);
if let Some((_, eval)) = eval {
return Some(HmacSecretSaltOrOutput::new(eval.first, eval.second));
}
}
let eval = request.eval?;
Some(HmacSecretSaltOrOutput::new(eval.first, eval.second))
}
#[cfg(test)]
pub mod tests;