1 // Copyright 2022 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 //! Nearby Presence-specific usage of LDT.
16 #![no_std]
17 
18 #[cfg(feature = "std")]
19 extern crate std;
20 
21 #[cfg(test)]
22 mod np_adv_test_vectors;
23 #[cfg(test)]
24 mod tests;
25 
26 use array_view::ArrayView;
27 use core::fmt;
28 use core::ops;
29 use crypto_provider::{aes::BLOCK_SIZE, CryptoProvider, CryptoRng, FromCryptoRng};
30 use ldt::{LdtCipher, LdtDecryptCipher, LdtEncryptCipher, LdtError, Swap, XorPadder};
31 use np_hkdf::{v0_ldt_expanded_salt, NpHmacSha256Key, NpKeySeedHkdf};
32 use xts_aes::XtsAes128;
33 
34 /// Max LDT-XTS-AES data size: `(2 * AES block size) - 1`
35 pub const LDT_XTS_AES_MAX_LEN: usize = 31;
36 
37 /// V0 format uses a 14-byte identity token
38 pub const V0_IDENTITY_TOKEN_LEN: usize = 14;
39 
40 /// Max payload size once identity token prefix has been removed
41 pub const NP_LDT_MAX_EFFECTIVE_PAYLOAD_LEN: usize = LDT_XTS_AES_MAX_LEN - V0_IDENTITY_TOKEN_LEN;
42 
43 /// Length of a V0 advertisement salt
44 pub const V0_SALT_LEN: usize = 2;
45 
46 /// The salt included in a V0 NP advertisement.
47 /// LDT does not use an IV but can instead incorporate the 2 byte, regularly rotated,
48 /// salt from the advertisement payload and XOR it with the padded tweak data.
49 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
50 pub struct V0Salt {
51     /// Salt bytes extracted from the incoming NP advertisement
52     bytes: [u8; V0_SALT_LEN],
53 }
54 
55 impl V0Salt {
56     /// Returns the salt as a byte array.
bytes(&self) -> [u8; V0_SALT_LEN]57     pub fn bytes(&self) -> [u8; V0_SALT_LEN] {
58         self.bytes
59     }
60 }
61 
62 impl From<[u8; V0_SALT_LEN]> for V0Salt {
from(arr: [u8; V0_SALT_LEN]) -> Self63     fn from(arr: [u8; V0_SALT_LEN]) -> Self {
64         Self { bytes: arr }
65     }
66 }
67 
68 /// "Short" 14-byte identity token type employed for V0
69 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
70 pub struct V0IdentityToken([u8; V0_IDENTITY_TOKEN_LEN]);
71 
72 impl V0IdentityToken {
73     /// Constructs a V0 identity token from raw bytes.
new(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self74     pub const fn new(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self {
75         Self(value)
76     }
77     /// Returns the underlying bytes
bytes(&self) -> [u8; V0_IDENTITY_TOKEN_LEN]78     pub fn bytes(&self) -> [u8; V0_IDENTITY_TOKEN_LEN] {
79         self.0
80     }
81 
82     /// Returns the token bytes as a slice
as_slice(&self) -> &[u8]83     pub fn as_slice(&self) -> &[u8] {
84         &self.0
85     }
86 }
87 
88 impl From<[u8; V0_IDENTITY_TOKEN_LEN]> for V0IdentityToken {
from(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self89     fn from(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self {
90         Self(value)
91     }
92 }
93 
94 impl AsRef<[u8]> for V0IdentityToken {
as_ref(&self) -> &[u8]95     fn as_ref(&self) -> &[u8] {
96         &self.0
97     }
98 }
99 
100 impl FromCryptoRng for V0IdentityToken {
new_random<R: CryptoRng>(rng: &mut R) -> Self101     fn new_random<R: CryptoRng>(rng: &mut R) -> Self {
102         Self(rng.gen())
103     }
104 }
105 
106 /// [LdtEncryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
107 pub type NpLdtEncryptCipher<C> = LdtEncryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
108 
109 /// [LdtDecryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
110 type NpLdtDecryptCipher<C> = LdtDecryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
111 
112 /// Range of valid NP LDT message lengths for encryption/decryption, in a convenient form that
113 /// doesn't need a CryptoProvider parameter.
114 pub const VALID_INPUT_LEN: ops::Range<usize> = BLOCK_SIZE..BLOCK_SIZE * 2;
115 
116 /// Build a Nearby Presence specific LDT XTS-AES-128 decrypter from a provided [NpKeySeedHkdf] and
117 /// metadata_key_hmac, with the [Swap] mix function
build_np_adv_decrypter_from_key_seed<C: CryptoProvider>( key_seed: &NpKeySeedHkdf<C>, identity_token_hmac: [u8; 32], ) -> AuthenticatedNpLdtDecryptCipher<C>118 pub fn build_np_adv_decrypter_from_key_seed<C: CryptoProvider>(
119     key_seed: &NpKeySeedHkdf<C>,
120     identity_token_hmac: [u8; 32],
121 ) -> AuthenticatedNpLdtDecryptCipher<C> {
122     build_np_adv_decrypter(
123         &key_seed.v0_ldt_key(),
124         identity_token_hmac,
125         key_seed.v0_identity_token_hmac_key(),
126     )
127 }
128 
129 /// Build a Nearby Presence specific LDT XTS-AES-128 decrypter from precalculated cipher components,
130 /// with the [Swap] mix function
build_np_adv_decrypter<C: CryptoProvider>( ldt_key: &ldt::LdtKey<xts_aes::XtsAes128Key>, identity_token_hmac: [u8; 32], identity_token_hmac_key: NpHmacSha256Key, ) -> AuthenticatedNpLdtDecryptCipher<C>131 pub fn build_np_adv_decrypter<C: CryptoProvider>(
132     ldt_key: &ldt::LdtKey<xts_aes::XtsAes128Key>,
133     identity_token_hmac: [u8; 32],
134     identity_token_hmac_key: NpHmacSha256Key,
135 ) -> AuthenticatedNpLdtDecryptCipher<C> {
136     AuthenticatedNpLdtDecryptCipher {
137         ldt_decrypter: NpLdtDecryptCipher::<C>::new(ldt_key),
138         metadata_key_tag: identity_token_hmac,
139         metadata_key_hmac_key: identity_token_hmac_key,
140     }
141 }
142 
143 /// Decrypts and validates a NP legacy format advertisement encrypted with LDT.
144 ///
145 /// A NP legacy advertisement will always be in the format of:
146 ///
147 /// Header (1 byte) | Salt (2 bytes) | Identity token (14 bytes) | repeated
148 /// { DE header | DE payload }
149 ///
150 /// Example:
151 /// Header (1 byte) | Salt (2 bytes) | Identity token (14 bytes) |
152 /// Tx power DE header (1 byte) | Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes)
153 ///
154 /// The ciphertext bytes will always start with the Identity through the end of the
155 /// advertisement, for example in the above [ Identity (14 bytes) | Tx power DE header (1 byte) |
156 /// Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes) ] will be the ciphertext section
157 /// passed as the input to `decrypt_and_verify`
158 ///
159 /// `B` is the underlying block cipher block size.
160 /// `O` is the max output size (must be 2 * B - 1).
161 /// `T` is the tweakable block cipher used by LDT.
162 /// `M` is the mix function used by LDT.
163 pub struct AuthenticatedNpLdtDecryptCipher<C: CryptoProvider> {
164     ldt_decrypter: LdtDecryptCipher<BLOCK_SIZE, XtsAes128<C>, Swap>,
165     metadata_key_tag: [u8; 32],
166     metadata_key_hmac_key: NpHmacSha256Key,
167 }
168 
169 impl<C: CryptoProvider> AuthenticatedNpLdtDecryptCipher<C> {
170     /// Decrypt an advertisement payload using the provided padder.
171     ///
172     /// If the plaintext's identity token matches this decrypter's MAC, returns the verified identity
173     /// token and the remaining plaintext (the bytes after the identity token).
174     ///
175     /// NOTE: because LDT acts as a PRP over the entire message, tampering with any bit scrambles
176     /// the whole message, so we can leverage the MAC on just the metadata key to ensure integrity
177     /// for the whole message.
178     ///
179     /// # Errors
180     /// - If `payload` has a length outside `[BLOCK_SIZE, BLOCK_SIZE * 2)`.
181     /// - If the decrypted plaintext fails its HMAC validation
182     #[allow(clippy::expect_used, clippy::indexing_slicing)]
decrypt_and_verify( &self, payload: &[u8], padder: &XorPadder<BLOCK_SIZE>, ) -> Result< (V0IdentityToken, ArrayView<u8, NP_LDT_MAX_EFFECTIVE_PAYLOAD_LEN>), LdtAdvDecryptError, >183     pub fn decrypt_and_verify(
184         &self,
185         payload: &[u8],
186         padder: &XorPadder<BLOCK_SIZE>,
187     ) -> Result<
188         (V0IdentityToken, ArrayView<u8, NP_LDT_MAX_EFFECTIVE_PAYLOAD_LEN>),
189         LdtAdvDecryptError,
190     > {
191         // we copy to avoid exposing plaintext that hasn't been validated w/ hmac
192         let mut buffer = [0_u8; LDT_XTS_AES_MAX_LEN];
193         let populated_buffer = buffer
194             .get_mut(..payload.len())
195             .ok_or(LdtAdvDecryptError::InvalidLength(payload.len()))?;
196         populated_buffer.copy_from_slice(payload);
197 
198         self.ldt_decrypter.decrypt(populated_buffer, padder).map_err(|e| match e {
199             LdtError::InvalidLength(l) => LdtAdvDecryptError::InvalidLength(l),
200         })?;
201         // slice is safe since input is a valid LDT-XTS-AES len
202         let identity_token = &populated_buffer[..V0_IDENTITY_TOKEN_LEN];
203         self.metadata_key_hmac_key
204             .verify_hmac::<C>(identity_token, self.metadata_key_tag)
205             .map_err(|_| LdtAdvDecryptError::MacMismatch)?;
206 
207         let token_arr: [u8; V0_IDENTITY_TOKEN_LEN] =
208             identity_token.try_into().expect("Length verified above");
209         Ok((
210             token_arr.into(),
211             ArrayView::try_from_slice(&buffer[V0_IDENTITY_TOKEN_LEN..payload.len()])
212                 .expect("Buffer len less token len is the max output len"),
213         ))
214     }
215 }
216 
217 /// Errors that can occur during [AuthenticatedNpLdtDecryptCipher::decrypt_and_verify].
218 #[derive(Debug, PartialEq, Eq)]
219 pub enum LdtAdvDecryptError {
220     /// The ciphertext data was an invalid length.
221     InvalidLength(usize),
222     /// The MAC calculated from the plaintext did not match the expected value
223     MacMismatch,
224 }
225 
226 impl fmt::Display for LdtAdvDecryptError {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result227     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228         match self {
229             LdtAdvDecryptError::InvalidLength(len) => {
230                 write!(f, "Adv decrypt error: invalid length ({len})")
231             }
232             LdtAdvDecryptError::MacMismatch => write!(f, "Adv decrypt error: MAC mismatch"),
233         }
234     }
235 }
236 
237 /// Build a XorPadder by HKDFing the NP advertisement salt
salt_padder<C: CryptoProvider>(salt: V0Salt) -> XorPadder<BLOCK_SIZE>238 pub fn salt_padder<C: CryptoProvider>(salt: V0Salt) -> XorPadder<BLOCK_SIZE> {
239     XorPadder::from(v0_ldt_expanded_salt::<C>(&salt.bytes))
240 }
241