1 // Copyright 2024 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //! Contains matched credential structs and traits which contain information about the credential 16 //! which is passed back to the caller upon a successful decrypt and credential match. 17 18 #[cfg(any(test, feature = "alloc"))] 19 use alloc::vec::Vec; 20 #[cfg(any(test, feature = "alloc"))] 21 use crypto_provider::CryptoProvider; 22 23 #[cfg(any(test, feature = "alloc"))] 24 use crate::credential::metadata::{decrypt_metadata_with_nonce, encrypt_metadata}; 25 use crate::credential::{v0::V0, v1::V1, ProtocolVersion}; 26 use core::{convert::Infallible, fmt::Debug}; 27 use ldt_np_adv::V0IdentityToken; 28 29 /// The portion of a credential's data to be bundled with the advertisement content it was used to 30 /// decrypt. At a minimum, this includes any encrypted identity-specific metadata. 31 /// 32 /// As it is `Debug` and `Eq`, implementors should not hold any cryptographic secrets to avoid 33 /// accidental logging, timing side channels on comparison, etc, or should use custom impls of 34 /// those traits rather than deriving them. 35 /// 36 /// Instances of `MatchedCredential` may be cloned whenever advertisement content is 37 /// successfully associated with a credential (see [`WithMatchedCredential`]). As a 38 /// result, it's recommended to use matched-credentials which reference 39 /// some underlying match-data, but don't necessarily own it. 40 /// See [`ReferencedMatchedCredential`] for the most common case of shared references. 41 pub trait MatchedCredential: Debug + PartialEq + Eq + Clone { 42 /// The type returned for successful calls to [`Self::fetch_encrypted_metadata`]. 43 type EncryptedMetadata: AsRef<[u8]>; 44 45 /// The type of errors for [`Self::fetch_encrypted_metadata`]. 46 type EncryptedMetadataFetchError: Debug; 47 48 /// Attempts to obtain the (AES-GCM)-encrypted metadata bytes for the credential, 49 /// with possible failure based on the availability of the underlying data (i.e: 50 /// failing disk reads.) 51 /// 52 /// If your implementation does not maintain any encrypted metadata for each credential, 53 /// you may simply return an empty byte-array from this method. 54 /// 55 /// If your method for obtaining metadata cannot fail, use 56 /// the `core::convert::Infallible` type for the error type 57 /// [`Self::EncryptedMetadataFetchError`]. fetch_encrypted_metadata( &self, ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>58 fn fetch_encrypted_metadata( 59 &self, 60 ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>; 61 } 62 63 /// [`MatchedCredential`] wrapper around a shared reference to a [`MatchedCredential`]. 64 /// This is done instead of providing a blanket impl of [`MatchedCredential`] for 65 /// reference types to allow for downstream crates to impl [`MatchedCredential`] on 66 /// specific reference types. 67 #[derive(Clone, Debug, PartialEq, Eq)] 68 pub struct ReferencedMatchedCredential<'a, M: MatchedCredential> { 69 wrapped: &'a M, 70 } 71 72 impl<'a, M: MatchedCredential> From<&'a M> for ReferencedMatchedCredential<'a, M> { from(wrapped: &'a M) -> Self73 fn from(wrapped: &'a M) -> Self { 74 Self { wrapped } 75 } 76 } 77 78 impl<'a, M: MatchedCredential> AsRef<M> for ReferencedMatchedCredential<'a, M> { as_ref(&self) -> &M79 fn as_ref(&self) -> &M { 80 self.wrapped 81 } 82 } 83 84 impl<'a, M: MatchedCredential> MatchedCredential for ReferencedMatchedCredential<'a, M> { 85 type EncryptedMetadata = <M as MatchedCredential>::EncryptedMetadata; 86 type EncryptedMetadataFetchError = <M as MatchedCredential>::EncryptedMetadataFetchError; fetch_encrypted_metadata( &self, ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>87 fn fetch_encrypted_metadata( 88 &self, 89 ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> { 90 self.wrapped.fetch_encrypted_metadata() 91 } 92 } 93 94 /// A simple implementation of [`MatchedCredential`] where all match-data 95 /// is contained in the encrypted metadata byte-field. 96 #[derive(Debug, PartialEq, Eq, Clone)] 97 pub struct MetadataMatchedCredential<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> { 98 encrypted_metadata: A, 99 } 100 101 #[cfg(any(test, feature = "alloc"))] 102 impl MetadataMatchedCredential<Vec<u8>> { 103 /// Builds a [`MetadataMatchedCredential`] whose contents are given 104 /// as plaintext to be encrypted using AES-GCM against the given 105 /// broadcast crypto-material. encrypt_from_plaintext<V, C>( hkdf: &np_hkdf::NpKeySeedHkdf<C>, identity_token: V::IdentityToken, plaintext_metadata: &[u8], ) -> Self where V: ProtocolVersion, C: CryptoProvider,106 pub fn encrypt_from_plaintext<V, C>( 107 hkdf: &np_hkdf::NpKeySeedHkdf<C>, 108 identity_token: V::IdentityToken, 109 plaintext_metadata: &[u8], 110 ) -> Self 111 where 112 V: ProtocolVersion, 113 C: CryptoProvider, 114 { 115 // TODO move this to identity provider 116 let encrypted_metadata = encrypt_metadata::<C, V>(hkdf, identity_token, plaintext_metadata); 117 Self { encrypted_metadata } 118 } 119 } 120 121 impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MetadataMatchedCredential<A> { 122 /// Builds a new [`MetadataMatchedCredential`] with the given 123 /// encrypted metadata. new(encrypted_metadata: A) -> Self124 pub fn new(encrypted_metadata: A) -> Self { 125 Self { encrypted_metadata } 126 } 127 } 128 129 impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MatchedCredential 130 for MetadataMatchedCredential<A> 131 { 132 type EncryptedMetadata = A; 133 type EncryptedMetadataFetchError = Infallible; fetch_encrypted_metadata(&self) -> Result<Self::EncryptedMetadata, Infallible>134 fn fetch_encrypted_metadata(&self) -> Result<Self::EncryptedMetadata, Infallible> { 135 Ok(self.encrypted_metadata.clone()) 136 } 137 } 138 139 /// Trivial implementation of [`MatchedCredential`] which consists of no match-data. 140 /// Suitable for usage scenarios where the decoded advertisement contents matter, 141 /// but not necessarily which devices generated the contents. 142 /// 143 /// Attempting to obtain the encrypted metadata from this type of credential 144 /// will always yield an empty byte-array. 145 #[derive(Default, Debug, PartialEq, Eq, Clone)] 146 pub struct EmptyMatchedCredential; 147 148 impl MatchedCredential for EmptyMatchedCredential { 149 type EncryptedMetadata = [u8; 0]; 150 type EncryptedMetadataFetchError = Infallible; fetch_encrypted_metadata( &self, ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>151 fn fetch_encrypted_metadata( 152 &self, 153 ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> { 154 Ok([0u8; 0]) 155 } 156 } 157 158 #[cfg(any(test, feature = "devtools"))] 159 /// A [`MatchedCredential`] which consists only of the `key_seed` in the crypto-material 160 /// for the credential. Note that this is unique per-credential by construction, 161 /// and so this provides natural match-data for credentials in settings where 162 /// there may not be any other information available. 163 /// 164 /// Since this matched-credential type contains cryptographic information mirroring 165 /// a credential's crypto-material, this structure is not suitable for production 166 /// usage outside of unit tests and dev-tools. 167 /// 168 /// Additionally, note that the metadata on this particular kind of matched credential 169 /// is deliberately made inaccessible. This is done because a key-seed representation 170 /// is only suitable in very limited circumstances where no other meaningful 171 /// identifying information is available, such as that which is contained in metadata. 172 /// Attempting to obtain the encrypted metadata from this type of matched credential 173 /// will always yield an empty byte-array. 174 #[derive(Default, Debug, PartialEq, Eq, Clone)] 175 pub struct KeySeedMatchedCredential { 176 key_seed: [u8; 32], 177 } 178 179 #[cfg(any(test, feature = "devtools"))] 180 impl From<[u8; 32]> for KeySeedMatchedCredential { from(key_seed: [u8; 32]) -> Self181 fn from(key_seed: [u8; 32]) -> Self { 182 Self { key_seed } 183 } 184 } 185 #[cfg(any(test, feature = "devtools"))] 186 impl From<KeySeedMatchedCredential> for [u8; 32] { from(matched: KeySeedMatchedCredential) -> Self187 fn from(matched: KeySeedMatchedCredential) -> Self { 188 matched.key_seed 189 } 190 } 191 192 #[cfg(any(test, feature = "devtools"))] 193 impl MatchedCredential for KeySeedMatchedCredential { 194 type EncryptedMetadata = [u8; 0]; 195 type EncryptedMetadataFetchError = Infallible; fetch_encrypted_metadata( &self, ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>196 fn fetch_encrypted_metadata( 197 &self, 198 ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> { 199 Ok([0u8; 0]) 200 } 201 } 202 203 /// Common trait to deserialized, decrypted V0 advs and V1 sections which 204 /// exposes relevant data about matched identities. 205 pub trait HasIdentityMatch { 206 /// The protocol version for which this advertisement 207 /// content has an identity-match. 208 type Version: ProtocolVersion; 209 210 /// Gets the decrypted plaintext version-specific 211 /// metadata key for the associated identity. identity_token(&self) -> <Self::Version as ProtocolVersion>::IdentityToken212 fn identity_token(&self) -> <Self::Version as ProtocolVersion>::IdentityToken; 213 } 214 215 impl HasIdentityMatch for V0IdentityToken { 216 type Version = V0; identity_token(&self) -> Self217 fn identity_token(&self) -> Self { 218 *self 219 } 220 } 221 222 impl HasIdentityMatch for crate::extended::V1IdentityToken { 223 type Version = V1; identity_token(&self) -> Self224 fn identity_token(&self) -> Self { 225 *self 226 } 227 } 228 229 #[cfg(any(test, feature = "alloc"))] 230 /// Type for errors from [`WithMatchedCredential#decrypt_metadata`] 231 #[derive(Debug)] 232 pub enum MatchedMetadataDecryptionError<M: MatchedCredential> { 233 /// Retrieving the encrypted metadata failed for one reason 234 /// or another, so we didn't get a chance to try decryption. 235 RetrievalFailed(<M as MatchedCredential>::EncryptedMetadataFetchError), 236 /// The encrypted metadata could be retrieved, but it did 237 /// not successfully decrypt against the matched identity. 238 /// This could be an indication of data corruption or 239 /// of malformed crypto on the sender-side. 240 DecryptionFailed, 241 } 242 243 /// Decrypted advertisement content with the [MatchedCredential] from the credential that decrypted 244 /// it, along with any other information which is relevant to the identity-match. 245 #[derive(Debug, PartialEq, Eq)] 246 pub struct WithMatchedCredential<M: MatchedCredential, T: HasIdentityMatch> { 247 matched: M, 248 /// The 12-byte metadata nonce as derived from the key-seed HKDF 249 /// to be used for decrypting the encrypted metadata in the attached 250 /// matched-credential. 251 metadata_nonce: [u8; 12], 252 contents: T, 253 } 254 255 impl<'a, M: MatchedCredential + Clone, T: HasIdentityMatch> 256 WithMatchedCredential<ReferencedMatchedCredential<'a, M>, T> 257 { 258 /// Clones the referenced match-data to update this container 259 /// so that the match-data is owned, rather than borrowed. clone_match_data(self) -> WithMatchedCredential<M, T>260 pub fn clone_match_data(self) -> WithMatchedCredential<M, T> { 261 let matched = self.matched.as_ref().clone(); 262 let metadata_nonce = self.metadata_nonce; 263 let contents = self.contents; 264 265 WithMatchedCredential { matched, metadata_nonce, contents } 266 } 267 } 268 269 impl<M: MatchedCredential, T: HasIdentityMatch> WithMatchedCredential<M, T> { new(matched: M, metadata_nonce: [u8; 12], contents: T) -> Self270 pub(crate) fn new(matched: M, metadata_nonce: [u8; 12], contents: T) -> Self { 271 Self { matched, metadata_nonce, contents } 272 } 273 /// Applies the given function to the wrapped contents, yielding 274 /// a new instance with the same matched-credential. map<R: HasIdentityMatch>( self, mapping: impl FnOnce(T) -> R, ) -> WithMatchedCredential<M, R>275 pub fn map<R: HasIdentityMatch>( 276 self, 277 mapping: impl FnOnce(T) -> R, 278 ) -> WithMatchedCredential<M, R> { 279 let contents = mapping(self.contents); 280 let matched = self.matched; 281 let metadata_nonce = self.metadata_nonce; 282 WithMatchedCredential { matched, metadata_nonce, contents } 283 } 284 /// Credential data for the credential that decrypted the content. matched_credential(&self) -> &M285 pub fn matched_credential(&self) -> &M { 286 &self.matched 287 } 288 /// The decrypted advertisement content. contents(&self) -> &T289 pub fn contents(&self) -> &T { 290 &self.contents 291 } 292 293 #[cfg(any(test, feature = "alloc"))] decrypt_metadata_from_fetch<C: CryptoProvider>( &self, encrypted_metadata: &[u8], ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>>294 fn decrypt_metadata_from_fetch<C: CryptoProvider>( 295 &self, 296 encrypted_metadata: &[u8], 297 ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> { 298 decrypt_metadata_with_nonce::<C, T::Version>( 299 self.metadata_nonce, 300 self.contents.identity_token(), 301 encrypted_metadata, 302 ) 303 .map_err(|_| MatchedMetadataDecryptionError::DecryptionFailed) 304 } 305 306 #[cfg(any(test, feature = "alloc"))] 307 /// Attempts to decrypt the encrypted metadata 308 /// associated with the matched credential 309 /// based on the details of the identity-match. decrypt_metadata<C: CryptoProvider>( &self, ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>>310 pub fn decrypt_metadata<C: CryptoProvider>( 311 &self, 312 ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> { 313 self.matched 314 .fetch_encrypted_metadata() 315 .map_err(|e| MatchedMetadataDecryptionError::RetrievalFailed(e)) 316 .and_then(|x| Self::decrypt_metadata_from_fetch::<C>(self, x.as_ref())) 317 } 318 } 319