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