| // 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())) | |
| } | |
| } |