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