blob: 90265d4e2f9a2a8652458ddc0bea77f76822c93e [file] [log] [blame]
// 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
//
// http://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.
//! Contains matched credential structs and traits which contain information about the credential
//! which is passed back to the caller upon a successful decrypt and credential match.
#[cfg(any(test, feature = "alloc"))]
use alloc::vec::Vec;
#[cfg(any(test, feature = "alloc"))]
use crypto_provider::CryptoProvider;
#[cfg(any(test, feature = "alloc"))]
use crate::credential::metadata::{decrypt_metadata_with_nonce, encrypt_metadata};
use crate::credential::{v0::V0, v1::V1, ProtocolVersion};
use core::{convert::Infallible, fmt::Debug};
use ldt_np_adv::V0IdentityToken;
/// The portion of a credential's data to be bundled with the advertisement content it was used to
/// decrypt. At a minimum, this includes any encrypted identity-specific metadata.
///
/// As it is `Debug` and `Eq`, implementors should not hold any cryptographic secrets to avoid
/// accidental logging, timing side channels on comparison, etc, or should use custom impls of
/// those traits rather than deriving them.
///
/// Instances of `MatchedCredential` may be cloned whenever advertisement content is
/// successfully associated with a credential (see [`WithMatchedCredential`]). As a
/// result, it's recommended to use matched-credentials which reference
/// some underlying match-data, but don't necessarily own it.
/// See [`ReferencedMatchedCredential`] for the most common case of shared references.
pub trait MatchedCredential: Debug + PartialEq + Eq + Clone {
/// The type returned for successful calls to [`Self::fetch_encrypted_metadata`].
type EncryptedMetadata: AsRef<[u8]>;
/// The type of errors for [`Self::fetch_encrypted_metadata`].
type EncryptedMetadataFetchError: Debug;
/// Attempts to obtain the (AES-GCM)-encrypted metadata bytes for the credential,
/// with possible failure based on the availability of the underlying data (i.e:
/// failing disk reads.)
///
/// If your implementation does not maintain any encrypted metadata for each credential,
/// you may simply return an empty byte-array from this method.
///
/// If your method for obtaining metadata cannot fail, use
/// the `core::convert::Infallible` type for the error type
/// [`Self::EncryptedMetadataFetchError`].
fn fetch_encrypted_metadata(
&self,
) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>;
}
/// [`MatchedCredential`] wrapper around a shared reference to a [`MatchedCredential`].
/// This is done instead of providing a blanket impl of [`MatchedCredential`] for
/// reference types to allow for downstream crates to impl [`MatchedCredential`] on
/// specific reference types.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReferencedMatchedCredential<'a, M: MatchedCredential> {
wrapped: &'a M,
}
impl<'a, M: MatchedCredential> From<&'a M> for ReferencedMatchedCredential<'a, M> {
fn from(wrapped: &'a M) -> Self {
Self { wrapped }
}
}
impl<'a, M: MatchedCredential> AsRef<M> for ReferencedMatchedCredential<'a, M> {
fn as_ref(&self) -> &M {
self.wrapped
}
}
impl<'a, M: MatchedCredential> MatchedCredential for ReferencedMatchedCredential<'a, M> {
type EncryptedMetadata = <M as MatchedCredential>::EncryptedMetadata;
type EncryptedMetadataFetchError = <M as MatchedCredential>::EncryptedMetadataFetchError;
fn fetch_encrypted_metadata(
&self,
) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
self.wrapped.fetch_encrypted_metadata()
}
}
/// A simple implementation of [`MatchedCredential`] where all match-data
/// is contained in the encrypted metadata byte-field.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct MetadataMatchedCredential<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> {
encrypted_metadata: A,
}
#[cfg(any(test, feature = "alloc"))]
impl MetadataMatchedCredential<Vec<u8>> {
/// Builds a [`MetadataMatchedCredential`] whose contents are given
/// as plaintext to be encrypted using AES-GCM against the given
/// broadcast crypto-material.
pub fn encrypt_from_plaintext<V, C>(
hkdf: &np_hkdf::NpKeySeedHkdf<C>,
identity_token: V::IdentityToken,
plaintext_metadata: &[u8],
) -> Self
where
V: ProtocolVersion,
C: CryptoProvider,
{
// TODO move this to identity provider
let encrypted_metadata = encrypt_metadata::<C, V>(hkdf, identity_token, plaintext_metadata);
Self { encrypted_metadata }
}
}
impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MetadataMatchedCredential<A> {
/// Builds a new [`MetadataMatchedCredential`] with the given
/// encrypted metadata.
pub fn new(encrypted_metadata: A) -> Self {
Self { encrypted_metadata }
}
}
impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MatchedCredential
for MetadataMatchedCredential<A>
{
type EncryptedMetadata = A;
type EncryptedMetadataFetchError = Infallible;
fn fetch_encrypted_metadata(&self) -> Result<Self::EncryptedMetadata, Infallible> {
Ok(self.encrypted_metadata.clone())
}
}
/// Trivial implementation of [`MatchedCredential`] which consists of no match-data.
/// Suitable for usage scenarios where the decoded advertisement contents matter,
/// but not necessarily which devices generated the contents.
///
/// Attempting to obtain the encrypted metadata from this type of credential
/// will always yield an empty byte-array.
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct EmptyMatchedCredential;
impl MatchedCredential for EmptyMatchedCredential {
type EncryptedMetadata = [u8; 0];
type EncryptedMetadataFetchError = Infallible;
fn fetch_encrypted_metadata(
&self,
) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
Ok([0u8; 0])
}
}
#[cfg(any(test, feature = "devtools"))]
/// A [`MatchedCredential`] which consists only of the `key_seed` in the crypto-material
/// for the credential. Note that this is unique per-credential by construction,
/// and so this provides natural match-data for credentials in settings where
/// there may not be any other information available.
///
/// Since this matched-credential type contains cryptographic information mirroring
/// a credential's crypto-material, this structure is not suitable for production
/// usage outside of unit tests and dev-tools.
///
/// Additionally, note that the metadata on this particular kind of matched credential
/// is deliberately made inaccessible. This is done because a key-seed representation
/// is only suitable in very limited circumstances where no other meaningful
/// identifying information is available, such as that which is contained in metadata.
/// Attempting to obtain the encrypted metadata from this type of matched credential
/// will always yield an empty byte-array.
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct KeySeedMatchedCredential {
key_seed: [u8; 32],
}
#[cfg(any(test, feature = "devtools"))]
impl From<[u8; 32]> for KeySeedMatchedCredential {
fn from(key_seed: [u8; 32]) -> Self {
Self { key_seed }
}
}
#[cfg(any(test, feature = "devtools"))]
impl From<KeySeedMatchedCredential> for [u8; 32] {
fn from(matched: KeySeedMatchedCredential) -> Self {
matched.key_seed
}
}
#[cfg(any(test, feature = "devtools"))]
impl MatchedCredential for KeySeedMatchedCredential {
type EncryptedMetadata = [u8; 0];
type EncryptedMetadataFetchError = Infallible;
fn fetch_encrypted_metadata(
&self,
) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
Ok([0u8; 0])
}
}
/// Common trait to deserialized, decrypted V0 advs and V1 sections which
/// exposes relevant data about matched identities.
pub trait HasIdentityMatch {
/// The protocol version for which this advertisement
/// content has an identity-match.
type Version: ProtocolVersion;
/// Gets the decrypted plaintext version-specific
/// metadata key for the associated identity.
fn identity_token(&self) -> <Self::Version as ProtocolVersion>::IdentityToken;
}
impl HasIdentityMatch for V0IdentityToken {
type Version = V0;
fn identity_token(&self) -> Self {
*self
}
}
impl HasIdentityMatch for crate::extended::V1IdentityToken {
type Version = V1;
fn identity_token(&self) -> Self {
*self
}
}
#[cfg(any(test, feature = "alloc"))]
/// Type for errors from [`WithMatchedCredential#decrypt_metadata`]
#[derive(Debug)]
pub enum MatchedMetadataDecryptionError<M: MatchedCredential> {
/// Retrieving the encrypted metadata failed for one reason
/// or another, so we didn't get a chance to try decryption.
RetrievalFailed(<M as MatchedCredential>::EncryptedMetadataFetchError),
/// The encrypted metadata could be retrieved, but it did
/// not successfully decrypt against the matched identity.
/// This could be an indication of data corruption or
/// of malformed crypto on the sender-side.
DecryptionFailed,
}
/// Decrypted advertisement content with the [MatchedCredential] from the credential that decrypted
/// it, along with any other information which is relevant to the identity-match.
#[derive(Debug, PartialEq, Eq)]
pub struct WithMatchedCredential<M: MatchedCredential, T: HasIdentityMatch> {
matched: M,
/// The 12-byte metadata nonce as derived from the key-seed HKDF
/// to be used for decrypting the encrypted metadata in the attached
/// matched-credential.
metadata_nonce: [u8; 12],
contents: T,
}
impl<'a, M: MatchedCredential + Clone, T: HasIdentityMatch>
WithMatchedCredential<ReferencedMatchedCredential<'a, M>, T>
{
/// Clones the referenced match-data to update this container
/// so that the match-data is owned, rather than borrowed.
pub fn clone_match_data(self) -> WithMatchedCredential<M, T> {
let matched = self.matched.as_ref().clone();
let metadata_nonce = self.metadata_nonce;
let contents = self.contents;
WithMatchedCredential { matched, metadata_nonce, contents }
}
}
impl<M: MatchedCredential, T: HasIdentityMatch> WithMatchedCredential<M, T> {
pub(crate) fn new(matched: M, metadata_nonce: [u8; 12], contents: T) -> Self {
Self { matched, metadata_nonce, contents }
}
/// Applies the given function to the wrapped contents, yielding
/// a new instance with the same matched-credential.
pub fn map<R: HasIdentityMatch>(
self,
mapping: impl FnOnce(T) -> R,
) -> WithMatchedCredential<M, R> {
let contents = mapping(self.contents);
let matched = self.matched;
let metadata_nonce = self.metadata_nonce;
WithMatchedCredential { matched, metadata_nonce, contents }
}
/// Credential data for the credential that decrypted the content.
pub fn matched_credential(&self) -> &M {
&self.matched
}
/// The decrypted advertisement content.
pub fn contents(&self) -> &T {
&self.contents
}
#[cfg(any(test, feature = "alloc"))]
fn decrypt_metadata_from_fetch<C: CryptoProvider>(
&self,
encrypted_metadata: &[u8],
) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> {
decrypt_metadata_with_nonce::<C, T::Version>(
self.metadata_nonce,
self.contents.identity_token(),
encrypted_metadata,
)
.map_err(|_| MatchedMetadataDecryptionError::DecryptionFailed)
}
#[cfg(any(test, feature = "alloc"))]
/// Attempts to decrypt the encrypted metadata
/// associated with the matched credential
/// based on the details of the identity-match.
pub fn decrypt_metadata<C: CryptoProvider>(
&self,
) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> {
self.matched
.fetch_encrypted_metadata()
.map_err(|e| MatchedMetadataDecryptionError::RetrievalFailed(e))
.and_then(|x| Self::decrypt_metadata_from_fetch::<C>(self, x.as_ref()))
}
}