use openssl::{hash, pkey, sign};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use crate::error::WebauthnError;
use crate::proto::CredentialID;
pub trait FakeCredentialIDDistribution {
fn generate<R: RngCore>(seeded_rng: &mut R) -> Vec<CredentialID>;
}
const CREDENTIAL_MEDIAN: u8 = 1;
pub const YUBIKEY_CRED_LEN: usize = 64;
pub const YUBIKEY_EDDSA_CRED_LEN: usize = 128;
pub const TPM_CRED_LEN: usize = 32;
pub const G_PIXEL_CRED_LEN: usize = 65;
pub const APPLE_CRED_LEN: usize = 20;
pub const BITWARDEN_CRED_LEN: usize = 16;
const SD_02: u32 = 94489281;
const SD_15: u32 = 678604833;
const SD_84: u32 = 3612067496;
const SD_98: u32 = 4200478015;
const CRED_SINGLE_68: u32 = 2946347564;
const CRED_SINGLE_98: u32 = 4200478014;
const CRED_MULTI_ANDROID: u32 = 2173682947;
const CRED_MULTI_WINDOWS: u32 = CRED_MULTI_ANDROID + 372803161;
const CRED_MULTI_APL: u32 = CRED_MULTI_WINDOWS + 1428076625;
const CRED_MULTI_YK: u32 = CRED_MULTI_APL + 160202280;
pub struct FakePasskeyDistribution;
impl FakeCredentialIDDistribution for FakePasskeyDistribution {
fn generate<R: RngCore>(seeded_rng: &mut R) -> Vec<CredentialID> {
let cred_dist = seeded_rng.next_u32();
let creds_to_generate = if cred_dist < SD_02 {
CREDENTIAL_MEDIAN.saturating_sub(2)
} else if cred_dist < SD_15 {
CREDENTIAL_MEDIAN.saturating_sub(1)
} else if cred_dist < SD_84 {
CREDENTIAL_MEDIAN
} else if cred_dist < SD_98 {
CREDENTIAL_MEDIAN.saturating_add(1)
} else {
CREDENTIAL_MEDIAN.saturating_add(2)
};
let mut credentials = Vec::with_capacity(creds_to_generate as usize);
if creds_to_generate == 1 {
let type_dist = seeded_rng.next_u32();
let cred_len = if type_dist < CRED_SINGLE_68 {
G_PIXEL_CRED_LEN
} else if type_dist < CRED_SINGLE_98 {
APPLE_CRED_LEN
} else {
BITWARDEN_CRED_LEN
};
let mut cred = vec![0; cred_len];
seeded_rng.fill_bytes(&mut cred);
credentials.push(cred.into());
} else {
for _i in 0..creds_to_generate {
let type_dist = seeded_rng.next_u32();
let cred_len = if type_dist < CRED_MULTI_ANDROID {
G_PIXEL_CRED_LEN
} else if type_dist < CRED_MULTI_WINDOWS {
TPM_CRED_LEN
} else if type_dist < CRED_MULTI_APL {
APPLE_CRED_LEN
} else if type_dist < CRED_MULTI_YK {
YUBIKEY_CRED_LEN
} else {
BITWARDEN_CRED_LEN
};
let mut cred = vec![0; cred_len];
seeded_rng.fill_bytes(&mut cred);
credentials.push(cred.into());
}
}
credentials
}
}
pub struct WebauthnFakeCredentialGenerator<D>
where
D: FakeCredentialIDDistribution,
{
hmac_key: pkey::PKey<pkey::Private>,
distribution: std::marker::PhantomData<D>,
}
impl<D> WebauthnFakeCredentialGenerator<D>
where
D: FakeCredentialIDDistribution,
{
pub fn new_hmac_key() -> Result<Vec<u8>, WebauthnError> {
let mut key = vec![0; 16];
openssl::rand::rand_bytes(&mut key)
.map_err(WebauthnError::OpenSSLError)
.map(|_| key)
}
}
impl<D> WebauthnFakeCredentialGenerator<D>
where
D: FakeCredentialIDDistribution,
{
pub fn new(hmac_key: &[u8]) -> Result<Self, WebauthnError> {
let hmac_key = pkey::PKey::hmac(hmac_key).map_err(WebauthnError::OpenSSLError)?;
Ok(WebauthnFakeCredentialGenerator {
hmac_key,
distribution: std::marker::PhantomData,
})
}
pub fn generate(&self, username: &[u8]) -> Result<Vec<CredentialID>, WebauthnError> {
let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &self.hmac_key)
.map_err(WebauthnError::OpenSSLError)?;
let mut seed = [0; 32];
let buf = signer
.sign_oneshot_to_vec(username)
.map_err(WebauthnError::OpenSSLError)?;
seed.copy_from_slice(&buf);
let mut seeded_rng = ChaCha8Rng::from_seed(seed);
let credentials = D::generate(&mut seeded_rng);
Ok(credentials)
}
}
#[cfg(test)]
mod tests {
use super::{FakePasskeyDistribution, WebauthnFakeCredentialGenerator};
use crate::proto::Base64UrlSafeData;
#[test]
fn test_fake_credential_generator() {
let _ = tracing_subscriber::fmt::try_init();
let cred_gen: WebauthnFakeCredentialGenerator<FakePasskeyDistribution> =
WebauthnFakeCredentialGenerator::new(&[0, 1, 2, 3]).unwrap();
let cred_a = cred_gen.generate(b"a").unwrap();
assert!(cred_a.is_empty());
let cred_b = cred_gen.generate(b"b").unwrap();
assert_eq!(
cred_b,
vec![Base64UrlSafeData::from(vec![
77, 7, 210, 37, 212, 90, 185, 162, 81, 110, 242, 185, 204, 84, 84, 123, 155, 139,
146, 230
])]
);
let cred_c = cred_gen.generate(b"c").unwrap();
assert!(cred_c.is_empty());
let cred_d = cred_gen.generate(b"d").unwrap();
assert_eq!(
cred_d,
vec![Base64UrlSafeData::from(vec![
203, 174, 48, 43, 223, 223, 211, 78, 99, 88, 240, 25, 90, 42, 86, 186, 239, 57,
123, 81, 177, 173, 236, 214, 204, 222, 224, 134, 233, 143, 143, 144, 127, 23, 26,
145, 217, 217, 110, 194, 235, 76, 2, 59, 56, 98, 47, 236, 103, 98, 235, 239, 195,
140, 199, 239, 201, 11, 132, 227, 181, 7, 188, 240, 168
])]
);
let cred_e = cred_gen.generate(b"e").unwrap();
assert_eq!(
cred_e,
vec![
Base64UrlSafeData::from(vec![
207, 79, 70, 16, 136, 39, 65, 40, 104, 116, 214, 85, 66, 12, 175, 99, 203, 228,
60, 249, 118, 169, 28, 217, 161, 132, 3, 217, 119, 66, 235, 151, 138, 15, 26,
76, 161, 44, 225, 120, 34, 131, 48, 195, 116, 81, 178, 0, 218, 96, 167, 1, 70,
183, 20, 94, 115, 63, 12, 235, 189, 105, 104, 60, 77
]),
Base64UrlSafeData::from(vec![
175, 118, 205, 177, 121, 39, 194, 157, 251, 53, 216, 180, 38, 22, 44, 155, 132,
155, 204, 68, 171, 98, 97, 114, 50, 58, 218, 238, 44, 154, 27, 140, 95, 90,
127, 210, 221, 177, 194, 44, 231, 178, 238, 239, 79, 222, 127, 164, 115, 65,
160, 6, 55, 150, 30, 140, 18, 159, 229, 159, 78, 216, 120, 27, 122
])
]
);
let alt_cred_gen: WebauthnFakeCredentialGenerator<FakePasskeyDistribution> =
WebauthnFakeCredentialGenerator::new(&[3, 2, 1, 0]).unwrap();
let alt_cred_a = alt_cred_gen.generate(b"a").unwrap();
assert_ne!(alt_cred_a, cred_a);
assert_eq!(
alt_cred_a,
vec![Base64UrlSafeData::from(vec![
44, 141, 39, 252, 47, 212, 48, 123, 96, 131, 15, 213, 21, 149, 95, 147, 188, 152,
201, 171, 245, 103, 22, 246, 211, 172, 143, 86, 97, 96, 109, 246, 23, 54, 13, 127,
167, 107, 72, 235, 151, 144, 162, 200, 251, 93, 137, 8, 211, 197, 47, 115, 108,
210, 62, 232, 246, 206, 36, 202, 94, 179, 254, 81, 59
])]
);
}
}