// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Passkey-related functionality in the enclave.
extern crate bytes;
extern crate chromesync;
extern crate crypto;
extern crate prost;
use super::{
debug, get_secret_from_request, open_aes_256_gcm, AuthLevel, Authentication, DirtyFlag,
OneTimeUV, PINState, ParsedState, Reauth, RequestError, SourceOfSecret, COUNTER_ID_KEY,
KEY_PURPOSE_SECURITY_DOMAIN_SECRET, PUB_KEY, VAULT_HANDLE_WITHOUT_TYPE_KEY,
WRAPPED_PIN_DATA_KEY, WRAPPED_SECRET_KEY,
};
use crate::pin;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use cbor::{MapKey, MapKeyRef, MapLookupKey, Value};
use chromesync::pb::webauthn_credential_specifics::EncryptedData;
use chromesync::pb::WebauthnCredentialSpecifics;
use core::ops::Deref;
use crypto::EcdsaKeyPair;
use prost::Message;
map_keys! {
CLAIMED_PIN, CLAIMED_PIN_KEY = "claimed_pin",
CLIENT_DATA_JSON, CLIENT_DATA_JSON_KEY = "client_data_json",
COSE_ALGORITHM, COSE_ALGORITHM_KEY = "alg",
ENCRYPTED, ENCRYPTED_KEY = "encrypted",
EVAL, EVAL_KEY = "eval",
EVAL_BY_CREDENTIAL, EVAL_BY_CREDENTIAL_KEY = "evalByCredential",
EXTENSIONS, EXTENSIONS_KEY = "extensions",
FIRST, FIRST_KEY = "first",
PIN_CLAIM_KEY, PIN_CLAIM_KEY_KEY = "pin_claim_key",
PIN_GENERATION, PIN_GENERATION_KEY = "pin_generation",
PIN_HASH, PIN_HASH_KEY = "pin_hash",
PRF, PRF_KEY = "prf",
PROTOBUF, PROTOBUF_KEY = "protobuf",
PUB_KEY_CRED_PARAMS, PUB_KEY_CRED_PARAMS_KEY = "pubKeyCredParams",
RP_ID, RP_ID_KEY = "rpId",
SECOND, SECOND_KEY = "second",
VERSION, VERSION_KEY = "version",
WEBAUTHN_REQUEST, WEBAUTHN_REQUEST_KEY = "request",
}
// The encrypted part of a WebauthnCredentialSpecifics sync entity is encrypted
// with AES-GCM and can come in two forms. These are the AAD inputs to AES-GCM
// that ensure domain separation.
const PRIVATE_KEY_FIELD_AAD: &[u8] = b"";
const ENCRYPTED_FIELD_AAD: &[u8] = b"WebauthnCredentialSpecifics.Encrypted";
pub(crate) const PIN_CLAIM_AAD: &[u8] = b"PIN claim";
// These constants are CTAP flags.
// See https://w3c.github.io/webauthn/#authdata-flags
const FLAG_USER_PRESENT: u8 = 1 << 0;
const FLAG_USER_VERIFIED: u8 = 1 << 2;
const FLAG_BACKUP_ELIGIBLE: u8 = 1 << 3;
const FLAG_BACKED_UP: u8 = 1 << 4;
// The signed authenticator data contains a four-byte signature counter. This
// is the special zero value that indicates that a counter is not supported.
// See https://w3c.github.io/webauthn/#authdata-signcount
const ZERO_SIGNATURE_COUNTER: &[u8; 4] = &[0u8; 4];
// https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier
const COSE_ALGORITHM_ECDSA_P256_SHA256: i64 = -7;
// The number of incorrect PIN attempts before further PIN attempts will be
// denied.
const MAX_PIN_ATTEMPTS: i64 = 5;
fn key(k: &str) -> MapKey {
MapKey::String(String::from(k))
}
pub(crate) fn do_assert(
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Authentication::Device(device_id, auth_level, one_time_uv, _) = auth else {
return debug("device identity required");
};
let Some(Value::Bytestring(proto_bytes)) = request.get(PROTOBUF_KEY) else {
return debug("protobuf required");
};
let Some(Value::String(client_data_json)) = request.get(CLIENT_DATA_JSON_KEY) else {
return debug("clientDataJSON required");
};
let client_data_json_hash = crypto::sha256(client_data_json.as_bytes());
let Some(Value::Map(webauthn_request)) = request.get(WEBAUTHN_REQUEST_KEY) else {
return debug("WebAuthn request required");
};
let Some(Value::String(rp_id)) = webauthn_request.get(RP_ID_KEY) else {
return debug("rpId required");
};
let rp_id_hash = crypto::sha256(rp_id.as_bytes());
let (security_domain_secret, secret_source) =
get_secret_from_request(state, &request, device_id)?;
let proto = WebauthnCredentialSpecifics::decode(proto_bytes.deref())
.map_err(|_| RequestError::Debug("failed to decode protobuf"))?;
let Some(ref credential_id) = proto.credential_id else {
return debug("protobuf is missing credential ID");
};
let Some(ref user_id) = proto.user_id else {
return debug("protobuf is missing user ID");
};
let entity_secrets = entity_secrets_from_proto(&security_domain_secret, &proto)?;
let pin_verified =
maybe_validate_pin_from_request(&request, state, device_id, &security_domain_secret)?;
let user_verification = matches!(auth_level, AuthLevel::UserVerification)
|| matches!(auth_level, AuthLevel::SoftwareUserVerification)
|| pin_verified
// If the client provided the security domain secret itself, then it could have
// done the signing itself too. Thus this is sufficient to claim UV.
|| matches!(secret_source, SourceOfSecret::Direct)
// A client can also nominate to get one free UV when registering their
// UV key, which is also sufficient for an assertion.
|| matches!(one_time_uv, OneTimeUV::Consumed);
let flags = [FLAG_BACKUP_ELIGIBLE
| FLAG_BACKED_UP
| FLAG_USER_PRESENT
| if user_verification { FLAG_USER_VERIFIED } else { 0 }];
let authenticator_data = [rp_id_hash.as_slice(), &flags, ZERO_SIGNATURE_COUNTER].concat();
let signed_data = [&authenticator_data, client_data_json_hash.as_slice()].concat();
let signature = entity_secrets
.primary_key
.sign(&signed_data)
.map_err(|_| RequestError::Debug("signing failed"))?;
// https://w3c.github.io/webauthn/#dictdef-authenticatorassertionresponsejson
let assertion_response_json = BTreeMap::<MapKey, Value>::from([
(key("clientDataJSON"), Value::String(client_data_json.clone())),
(key("authenticatorData"), Value::from(authenticator_data)),
(key("signature"), Value::from(signature.as_ref())),
(key("userHandle"), Value::from(user_id.to_vec())),
]);
let mut response = BTreeMap::from([(key("response"), Value::Map(assertion_response_json))]);
if let Some(ref hmac_secret) = entity_secrets.hmac_secret {
if let Some(prf_result) =
handle_prf(webauthn_request, hmac_secret, Some(credential_id.as_ref()))?
{
response.insert(key(PRF), prf_result);
}
}
Ok(Value::Map(response))
}
pub(crate) fn do_create(
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Authentication::Device(device_id, _, _, _) = auth else {
return debug("device identity required");
};
let (security_domain_secret, _) = get_secret_from_request(state, &request, device_id)?;
let Some(Value::Map(webauthn_request)) = request.get(WEBAUTHN_REQUEST_KEY) else {
return debug("WebAuthn request required");
};
let Some(Value::Array(pub_key_cred_params)) = webauthn_request.get(PUB_KEY_CRED_PARAMS_KEY)
else {
return debug("missing pubKeyCredParams array");
};
let cose_algorithms: Result<Vec<i64>, RequestError> = pub_key_cred_params
.iter()
.map(|cred_param| match cred_param {
Value::Map(i) => {
let Some(Value::Int(alg)) = i.get(COSE_ALGORITHM_KEY) else {
return debug("missing algorithm");
};
Ok(*alg)
}
_ => debug("invalid algorithm type"),
})
.collect();
// I can't get Rust to accept merging this "?" into the previous line.
let cose_algorithms = cose_algorithms?;
if !cose_algorithms.contains(&COSE_ALGORITHM_ECDSA_P256_SHA256) {
return Err(RequestError::NoSupportedAlgorithm);
}
// Creating a credential doesn't sign anything, so the return value here
// isn't used. But an incorrect PIN will still cause the request to fail
// so that the client can check whether it was correct.
maybe_validate_pin_from_request(&request, state, device_id, &security_domain_secret)?;
let pkcs8 = EcdsaKeyPair::generate_pkcs8();
let key = EcdsaKeyPair::from_pkcs8(pkcs8.as_ref())
.map_err(|_| RequestError::Debug("failed to parse private key"))?;
let pub_key = key.public_key();
let mut hmac_secret = [0u8; 32];
crypto::rand_bytes(&mut hmac_secret);
let pb = chromesync::pb::webauthn_credential_specifics::Encrypted {
private_key: Some(pkcs8.as_ref().to_vec()),
hmac_secret: Some(hmac_secret.to_vec()),
cred_blob: None,
large_blob: None,
large_blob_uncompressed_size: None,
};
let ciphertext = encrypt(&security_domain_secret, pb.encode_to_vec(), ENCRYPTED_FIELD_AAD)?;
let mut result = BTreeMap::from([
(MapKey::String(String::from(ENCRYPTED)), Value::from(ciphertext)),
(MapKey::String(String::from(PUB_KEY)), Value::from(pub_key.as_ref().to_vec())),
]);
if let Some(prf_result) = handle_prf(webauthn_request, &hmac_secret, None)? {
result.insert(MapKey::String(String::from(PRF)), prf_result);
}
Ok(Value::Map(result))
}
/// Attempt to verify a claimed PIN from `request`. Returns true if the PIN
/// verified correctly, false if there was no PIN claim, and an error if
/// the PIN claim was invalid for any reason.
fn maybe_validate_pin_from_request(
request: &BTreeMap<MapKey, Value>,
state: &mut DirtyFlag<'_, ParsedState>,
device_id: &[u8],
security_domain_secret: &[u8; 32],
) -> Result<bool, RequestError> {
if let Some(Value::Bytestring(wrapped_pin_data)) = request.get(WRAPPED_PIN_DATA_KEY) {
let Some(Value::Bytestring(claimed_pin)) = request.get(CLAIMED_PIN_KEY) else {
return debug("claimed PIN required");
};
validate_pin(
state,
device_id,
security_domain_secret.as_ref(),
claimed_pin,
wrapped_pin_data,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Contains the secrets from a specific passkey Sync entity.
struct EntitySecrets {
primary_key: EcdsaKeyPair,
hmac_secret: Option<[u8; 32]>,
// These fields are not yet implemented but are contained in the protobuf
// definition.
// cred_blob: Option<Vec<u8>>,
// large_blob: Option<(Vec<u8>, u64)>,
}
/// Given a list of wrapped secrets and the wrapping key for this device,
/// returns the entity secrets from a protobuf and the security domain secret
/// used.
fn entity_secrets_from_proto(
security_domain_secret: &[u8; 32],
proto: &WebauthnCredentialSpecifics,
) -> Result<EntitySecrets, RequestError> {
let Some(encrypted_data) = &proto.encrypted_data else {
return debug("sync entity missing encrypted data");
};
match encrypted_data {
EncryptedData::PrivateKey(ciphertext) => {
let plaintext = decrypt(ciphertext, security_domain_secret, PRIVATE_KEY_FIELD_AAD)?;
let primary_key = EcdsaKeyPair::from_pkcs8(&plaintext)
.map_err(|_| RequestError::Debug("PKCS#8 parse failed"))?;
Ok(EntitySecrets { primary_key, hmac_secret: None })
}
EncryptedData::Encrypted(ciphertext) => {
let plaintext = decrypt(ciphertext, security_domain_secret, ENCRYPTED_FIELD_AAD)?;
let encrypted = chromesync::pb::webauthn_credential_specifics::Encrypted::decode(
plaintext.as_slice(),
)
.map_err(|_| RequestError::Debug("failed to decode encrypted data"))?;
let Some(private_key_bytes) = encrypted.private_key else {
return debug("missing private key");
};
let primary_key = EcdsaKeyPair::from_pkcs8(&private_key_bytes)
.map_err(|_| RequestError::Debug("PKCS#8 parse failed"))?;
let hmac_secret = encrypted.hmac_secret.and_then(|vec| vec.try_into().ok());
Ok(EntitySecrets { primary_key, hmac_secret })
}
}
}
/// Encrypt an entity secret using a security domain secret.
fn encrypt(
security_domain_secret: &[u8; 32],
mut plaintext: Vec<u8>,
aad: &[u8],
) -> Result<Vec<u8>, RequestError> {
let mut encryption_key = [0u8; 32];
security_domain_secret_to_encryption_key(security_domain_secret, &mut encryption_key);
let mut nonce_bytes = [0u8; 12];
crypto::rand_bytes(&mut nonce_bytes);
crypto::aes_256_gcm_seal_in_place(&encryption_key, &nonce_bytes, aad, &mut plaintext);
Ok([nonce_bytes.as_slice(), &plaintext].concat())
}
/// Decrypt an entity secret using a security domain secret.
///
/// Different entity secrets will have different "additional authenticated
/// data" values to ensure that ciphertexts are interpreted in their correct
/// context.
fn decrypt(
ciphertext: &[u8],
security_domain_secret: &[u8; 32],
aad: &[u8],
) -> Result<Vec<u8>, RequestError> {
let mut encryption_key = [0u8; 32];
security_domain_secret_to_encryption_key(security_domain_secret, &mut encryption_key);
open_aes_256_gcm(&encryption_key, ciphertext, aad)
.ok_or(RequestError::Debug("decryption failed"))
}
/// Derive the key used for encrypting entity secrets from the security domain
/// secret.
fn security_domain_secret_to_encryption_key(
security_domain_secret: &[u8; 32],
out_passkey_key: &mut [u8; 32],
) {
// unwrap: only fails if the output length is too long, but we know that
// `out_passkey_key` is 32 bytes.
crypto::hkdf_sha256(
security_domain_secret,
&[],
b"KeychainApplicationKey:gmscore_module:com.google.android.gms.fido",
out_passkey_key,
)
.unwrap();
}
/// Compares two secrets for equality.
fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
// There is a crate called `constant_time_eq` that uses lots of magic to
// try and stop Rust from optimising away the constant-time compare. I
// can't judge whether that magic is sufficient nor, if it is today,
// whether it'll continue to be in the future. Since this operation isn't
// performance-sensitive in the context that it's used here, this code
// does a randomised hash of the secret values and compares the digests.
// That's very slow, but it's safe.
let mut rand_bytes = [0; 32];
crypto::rand_bytes(&mut rand_bytes);
crypto::sha256_two_part(&rand_bytes, a) == crypto::sha256_two_part(&rand_bytes, b)
}
/// Validate a claimed PIN, returning an error if it's incorrect.
fn validate_pin(
state: &mut DirtyFlag<ParsedState>,
device_id: &[u8],
security_domain_secret: &[u8],
claim: &[u8],
wrapped_pin_data: &[u8],
) -> Result<(), RequestError> {
let PINState { attempts } = state.get_pin_state(device_id)?;
if attempts >= MAX_PIN_ATTEMPTS {
return Err(RequestError::PINLocked);
}
let pin_data = pin::Data::from_wrapped(wrapped_pin_data, security_domain_secret)?;
let claimed_pin_hash = open_aes_256_gcm(&pin_data.claim_key, claim, PIN_CLAIM_AAD)
.ok_or(RequestError::Debug("failed to decrypt PIN claim"))?;
if !constant_time_compare(&claimed_pin_hash, &pin_data.pin_hash) {
state.get_mut().set_pin_state(device_id, PINState { attempts: attempts + 1 })?;
return Err(RequestError::IncorrectPIN);
}
// These is an availability / security tradeoff here. In the case of a correct
// PIN guess, we don't serialise the updated state. Thus, if a user's machine
// has malware that can grab the needed secrets it can try to rush more than
// `MAX_PIN_ATTEMPTS` guesses at the enclave service. If the storage system is
// having trouble it's possible that more than `MAX_PIN_ATTEMPTS` will be
// considered. However, if we serialize these state updates then every time the
// storage system has a glitch, nobody will be able to validate assertions with
// a PIN. Since the attack requires malware on the client machine, where the
// user could probably be phished for their PIN much more effectively than
// trying to exploit a concurrency issue, we err on the side of availability.
if attempts > 0 {
state.get_mut_for_minor_change().set_pin_state(device_id, PINState { attempts: 0 })?;
}
Ok(())
}
pub(crate) fn do_wrap_pin(
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
// Reauth is required to perform this command.
let device_id = match auth {
Authentication::Device(device_id, _, _, Reauth::Done) => device_id,
_ => return debug("PIN change needs reauth via RAPT token"),
};
let Some(Value::Bytestring(pin_hash)) = request.get(PIN_HASH_KEY) else {
return debug("pin_hash required");
};
let Some(Value::Int(generation)) = request.get(PIN_GENERATION_KEY) else {
return debug("pin_generation required");
};
let Some(Value::Bytestring(claim_key)) = request.get(PIN_CLAIM_KEY_KEY) else {
return debug("pin_claim_key required");
};
let Some(Value::Bytestring(wrapped_secret)) = request.get(WRAPPED_SECRET_KEY) else {
return debug("wrapped secret required");
};
let Some(Value::Bytestring(counter_id)) = request.get(COUNTER_ID_KEY) else {
return debug("counter ID required");
};
let Some(Value::Bytestring(vault_handle_without_type)) =
request.get(VAULT_HANDLE_WITHOUT_TYPE_KEY)
else {
return debug("vault handle required");
};
let security_domain_secret =
state.unwrap(device_id, wrapped_secret, KEY_PURPOSE_SECURITY_DOMAIN_SECRET)?;
let pin_data = pin::Data {
pin_hash: pin_hash
.as_ref()
.try_into()
.map_err(|_| RequestError::Debug("incorrect length PIN hash"))?,
generation: *generation,
claim_key: claim_key
.as_ref()
.try_into()
.map_err(|_| RequestError::Debug("incorrect length claim key"))?,
counter_id: counter_id
.as_ref()
.try_into()
.map_err(|_| RequestError::Debug("incorrect length counter id"))?,
vault_handle_without_type: vault_handle_without_type
.as_ref()
.try_into()
.map_err(|_| RequestError::Debug("incorrect length vault handle"))?,
};
Ok(Value::from(pin_data.encrypt(&security_domain_secret)))
}
/// PRFValues mirrors `AuthenticationExtensionsPRFValues` from the WebAuthn
/// spec, although it contains either post-hashed values or HMAC outputs.
struct PRFValues {
first: [u8; 32],
second: Option<[u8; 32]>,
}
impl PRFValues {
/// Treat a `PRFValues` as evaluation points and evaluate them for a given
/// HMAC key.
fn hmac(self, hmac_key: &[u8; 32]) -> Self {
PRFValues {
first: crypto::hmac_sha256(hmac_key, &self.first),
second: self.second.map(|second| crypto::hmac_sha256(hmac_key, &second)),
}
}
/// Convert to a CBOR structure.
fn into_cbor(self) -> Value {
let mut ret = BTreeMap::from([(key(FIRST), Value::from(&self.first))]);
if let Some(second) = self.second {
ret.insert(key(SECOND), Value::from(&second));
}
Value::Map(ret)
}
}
impl TryFrom<&Value> for PRFValues {
type Error = RequestError;
/// Attempt to parse a PRFValues from a CBOR input that reflects a
/// `AuthenticationExtensionsPRFValues` WebAuthn structure.
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let Value::Map(prf) = v else {
return debug("PRF value is not a map");
};
fn get_value(value: &Value) -> Result<[u8; 32], RequestError> {
let Value::String(value) = value else {
return debug("invalid PRF value");
};
let value = base64::decode_config(value, base64::URL_SAFE_NO_PAD)
.map_err(|_| RequestError::Debug("invalid PRF base64url"))?;
Ok(hash_prf_value(&value))
}
let first =
get_value(prf.get(FIRST_KEY).ok_or(RequestError::Debug("missing PRF first value"))?)?;
let second = prf.get(SECOND_KEY).map(get_value).transpose()?;
Ok(PRFValues { first, second })
}
}
/// Map WebAuthn-scoped PRF inputs to raw values.
///
/// The PRF inputs that a website is allowed to evaluate are limited to the
/// images of a hash function so that different parties can be given different
/// evaluation powers. See https://w3c.github.io/webauthn/#prf-extension
fn hash_prf_value(input: &[u8]) -> [u8; 32] {
const PREFIX: &[u8] = b"WebAuthn PRF\x00";
crypto::sha256_two_part(PREFIX, input)
}
/// Optionally handle the PRF extension from a request given an HMAC key. If the
/// request is an assertion then `credential_id` specifies the credential being
/// evaluated.
fn handle_prf(
webauthn_request: &BTreeMap<MapKey, Value>,
hmac_secret: &[u8; 32],
credential_id: Option<&[u8]>,
) -> Result<Option<Value>, RequestError> {
let Some(Value::Map(extensions)) = webauthn_request.get(EXTENSIONS_KEY) else {
return Ok(None);
};
let Some(Value::Map(prf)) = extensions.get(PRF_KEY) else {
return Ok(None);
};
if credential_id.is_none() && prf.is_empty() {
return Ok(Some(Value::Boolean(true)));
}
Ok(prf_values_by_id(prf, credential_id)?
.or(prf_default_values(prf)?)
.map(|values| values.hmac(hmac_secret))
.map(PRFValues::into_cbor))
}
/// Get the PRF values for a given credential ID, if any.
fn prf_values_by_id(
prf: &BTreeMap<MapKey, Value>,
credential_id: Option<&[u8]>,
) -> Result<Option<PRFValues>, RequestError> {
let Some(credential_id) = credential_id else {
return Ok(None);
};
let Some(Value::Map(by_credential)) = prf.get(EVAL_BY_CREDENTIAL_KEY) else {
return Ok(None);
};
let base64url_credential_id = base64::encode_config(credential_id, base64::URL_SAFE_NO_PAD);
let Some(values) = by_credential.get(&MapKey::String(base64url_credential_id)) else {
return Ok(None);
};
Ok(Some(values.try_into()?))
}
/// Get the default PRF values from the extension, if any.
fn prf_default_values(prf: &BTreeMap<MapKey, Value>) -> Result<Option<PRFValues>, RequestError> {
let Some(eval) = prf.get(EVAL_KEY) else {
return Ok(None);
};
Ok(Some(eval.try_into()?))
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::recovery_key_store;
use crate::tests::{
PROTOBUF2_BYTES, PROTOBUF_BYTES, SAMPLE_SECURITY_DOMAIN_SECRET,
WEBAUTHN_SECRETS_ENCRYPTION_KEY,
};
lazy_static! {
static ref PROTOBUF: WebauthnCredentialSpecifics =
WebauthnCredentialSpecifics::decode(PROTOBUF_BYTES).unwrap();
static ref PROTOBUF2: WebauthnCredentialSpecifics =
WebauthnCredentialSpecifics::decode(PROTOBUF2_BYTES).unwrap();
}
#[test]
fn test_decrypt() {
let Some(EncryptedData::PrivateKey(ciphertext)) = &PROTOBUF.encrypted_data else {
panic!("bad protobuf");
};
let pkcs8 =
decrypt(&ciphertext, SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(), &[]).unwrap();
assert!(EcdsaKeyPair::from_pkcs8(&pkcs8).is_ok());
assert!(
decrypt(&[0u8; 8], SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(), &[]).is_err()
);
}
#[test]
fn test_entity_secrets_from_proto() {
let protobuf1: &WebauthnCredentialSpecifics = &PROTOBUF;
let protobuf2: &WebauthnCredentialSpecifics = &PROTOBUF2;
for (n, proto) in [protobuf1, protobuf2].iter().enumerate() {
let result =
entity_secrets_from_proto(SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(), proto);
assert!(result.is_ok(), "{:?}", proto);
let result = result.unwrap();
let should_have_hmac_secret = n == 1;
assert_eq!(matches!(result.hmac_secret, Some(_)), should_have_hmac_secret);
}
}
#[test]
fn test_encrypt() {
let plaintext = b"hello";
let ciphertext =
encrypt(SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(), plaintext.to_vec(), &[])
.unwrap();
let plaintext2 =
decrypt(&ciphertext, SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(), &[]).unwrap();
assert_eq!(plaintext, plaintext2.as_slice());
}
#[test]
fn test_security_domain_secret_to_encryption_key() {
let mut calculated = [0u8; 32];
security_domain_secret_to_encryption_key(
SAMPLE_SECURITY_DOMAIN_SECRET.try_into().unwrap(),
&mut calculated,
);
assert_eq!(WEBAUTHN_SECRETS_ENCRYPTION_KEY, &calculated);
}
#[test]
fn test_constant_time_compare() {
struct Test {
a: &'static [u8],
b: &'static [u8],
expected: bool,
}
let tests: &[Test] = &[
Test { a: b"", b: b"", expected: true },
Test { a: b"a", b: b"", expected: false },
Test { a: b"", b: b"b", expected: false },
Test { a: b"a", b: b"b", expected: false },
Test { a: b"a", b: b"a", expected: true },
Test { a: b"abcde", b: b"abcde", expected: true },
Test { a: b"abcdf", b: b"abcde", expected: false },
];
for (i, test) in tests.iter().enumerate() {
if constant_time_compare(test.a, test.b) != test.expected {
panic!("failed at #{}", i)
}
}
}
#[test]
fn test_pin_data() {
let pin_data = pin::Data {
pin_hash: [1u8; 32],
generation: 42,
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
};
let pin_data2: pin::Data = pin_data.to_bytes().try_into().unwrap();
assert_eq!(pin_data, pin_data2);
let security_domain_secret = [3u8; 32];
let encrypted = pin_data.encrypt(&security_domain_secret);
let decrypted = pin::Data::from_wrapped(&encrypted, &security_domain_secret).unwrap();
assert_eq!(pin_data, decrypted);
}
// Integration tests of this code are done in Chromium, which builds this
// code and runs it against the client code to ensure that, e.g.,
// assertions work end-to-end.
}