blob: 1d202f796b56f2a58590b0274f56a0bb54e9dda9 [file] [log] [blame]
// Copyright 2022 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.
//! Nearby Presence-specific usage of LDT.
#![no_std]
#[cfg(feature = "std")]
extern crate std;
#[cfg(test)]
mod np_adv_test_vectors;
#[cfg(test)]
mod tests;
use array_view::ArrayView;
use core::fmt;
use core::ops;
use crypto_provider::{aes::BLOCK_SIZE, CryptoProvider, CryptoRng, FromCryptoRng};
use ldt::{LdtCipher, LdtDecryptCipher, LdtEncryptCipher, LdtError, Swap, XorPadder};
use np_hkdf::{v0_ldt_expanded_salt, NpHmacSha256Key, NpKeySeedHkdf};
use xts_aes::XtsAes128;
/// Max LDT-XTS-AES data size: `(2 * AES block size) - 1`
pub const LDT_XTS_AES_MAX_LEN: usize = 31;
/// V0 format uses a 14-byte identity token
pub const V0_IDENTITY_TOKEN_LEN: usize = 14;
/// Max payload size once identity token prefix has been removed
pub const NP_LDT_MAX_EFFECTIVE_PAYLOAD_LEN: usize = LDT_XTS_AES_MAX_LEN - V0_IDENTITY_TOKEN_LEN;
/// Length of a V0 advertisement salt
pub const V0_SALT_LEN: usize = 2;
/// The salt included in a V0 NP advertisement.
/// LDT does not use an IV but can instead incorporate the 2 byte, regularly rotated,
/// salt from the advertisement payload and XOR it with the padded tweak data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V0Salt {
/// Salt bytes extracted from the incoming NP advertisement
bytes: [u8; V0_SALT_LEN],
}
impl V0Salt {
/// Returns the salt as a byte array.
pub fn bytes(&self) -> [u8; V0_SALT_LEN] {
self.bytes
}
}
impl From<[u8; V0_SALT_LEN]> for V0Salt {
fn from(arr: [u8; V0_SALT_LEN]) -> Self {
Self { bytes: arr }
}
}
/// "Short" 14-byte identity token type employed for V0
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct V0IdentityToken([u8; V0_IDENTITY_TOKEN_LEN]);
impl V0IdentityToken {
/// Constructs a V0 identity token from raw bytes.
pub const fn new(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self {
Self(value)
}
/// Returns the underlying bytes
pub fn bytes(&self) -> [u8; V0_IDENTITY_TOKEN_LEN] {
self.0
}
/// Returns the token bytes as a slice
pub fn as_slice(&self) -> &[u8] {
&self.0
}
}
impl From<[u8; V0_IDENTITY_TOKEN_LEN]> for V0IdentityToken {
fn from(value: [u8; V0_IDENTITY_TOKEN_LEN]) -> Self {
Self(value)
}
}
impl AsRef<[u8]> for V0IdentityToken {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl FromCryptoRng for V0IdentityToken {
fn new_random<R: CryptoRng>(rng: &mut R) -> Self {
Self(rng.gen())
}
}
/// [LdtEncryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
pub type NpLdtEncryptCipher<C> = LdtEncryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
/// [LdtDecryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
type NpLdtDecryptCipher<C> = LdtDecryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
/// Range of valid NP LDT message lengths for encryption/decryption, in a convenient form that
/// doesn't need a CryptoProvider parameter.
pub const VALID_INPUT_LEN: ops::Range<usize> = BLOCK_SIZE..BLOCK_SIZE * 2;
/// Build a Nearby Presence specific LDT XTS-AES-128 decrypter from a provided [NpKeySeedHkdf] and
/// metadata_key_hmac, with the [Swap] mix function
pub fn build_np_adv_decrypter_from_key_seed<C: CryptoProvider>(
key_seed: &NpKeySeedHkdf<C>,
identity_token_hmac: [u8; 32],
) -> AuthenticatedNpLdtDecryptCipher<C> {
build_np_adv_decrypter(
&key_seed.v0_ldt_key(),
identity_token_hmac,
key_seed.v0_identity_token_hmac_key(),
)
}
/// Build a Nearby Presence specific LDT XTS-AES-128 decrypter from precalculated cipher components,
/// with the [Swap] mix function
pub fn 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> {
AuthenticatedNpLdtDecryptCipher {
ldt_decrypter: NpLdtDecryptCipher::<C>::new(ldt_key),
metadata_key_tag: identity_token_hmac,
metadata_key_hmac_key: identity_token_hmac_key,
}
}
/// Decrypts and validates a NP legacy format advertisement encrypted with LDT.
///
/// A NP legacy advertisement will always be in the format of:
///
/// Header (1 byte) | Salt (2 bytes) | Identity token (14 bytes) | repeated
/// { DE header | DE payload }
///
/// Example:
/// Header (1 byte) | Salt (2 bytes) | Identity token (14 bytes) |
/// Tx power DE header (1 byte) | Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes)
///
/// The ciphertext bytes will always start with the Identity through the end of the
/// advertisement, for example in the above [ Identity (14 bytes) | Tx power DE header (1 byte) |
/// Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes) ] will be the ciphertext section
/// passed as the input to `decrypt_and_verify`
///
/// `B` is the underlying block cipher block size.
/// `O` is the max output size (must be 2 * B - 1).
/// `T` is the tweakable block cipher used by LDT.
/// `M` is the mix function used by LDT.
pub struct AuthenticatedNpLdtDecryptCipher<C: CryptoProvider> {
ldt_decrypter: LdtDecryptCipher<BLOCK_SIZE, XtsAes128<C>, Swap>,
metadata_key_tag: [u8; 32],
metadata_key_hmac_key: NpHmacSha256Key,
}
impl<C: CryptoProvider> AuthenticatedNpLdtDecryptCipher<C> {
/// Decrypt an advertisement payload using the provided padder.
///
/// If the plaintext's identity token matches this decrypter's MAC, returns the verified identity
/// token and the remaining plaintext (the bytes after the identity token).
///
/// NOTE: because LDT acts as a PRP over the entire message, tampering with any bit scrambles
/// the whole message, so we can leverage the MAC on just the metadata key to ensure integrity
/// for the whole message.
///
/// # Errors
/// - If `payload` has a length outside `[BLOCK_SIZE, BLOCK_SIZE * 2)`.
/// - If the decrypted plaintext fails its HMAC validation
#[allow(clippy::expect_used, clippy::indexing_slicing)]
pub fn decrypt_and_verify(
&self,
payload: &[u8],
padder: &XorPadder<BLOCK_SIZE>,
) -> Result<
(V0IdentityToken, ArrayView<u8, NP_LDT_MAX_EFFECTIVE_PAYLOAD_LEN>),
LdtAdvDecryptError,
> {
// we copy to avoid exposing plaintext that hasn't been validated w/ hmac
let mut buffer = [0_u8; LDT_XTS_AES_MAX_LEN];
let populated_buffer = buffer
.get_mut(..payload.len())
.ok_or(LdtAdvDecryptError::InvalidLength(payload.len()))?;
populated_buffer.copy_from_slice(payload);
self.ldt_decrypter.decrypt(populated_buffer, padder).map_err(|e| match e {
LdtError::InvalidLength(l) => LdtAdvDecryptError::InvalidLength(l),
})?;
// slice is safe since input is a valid LDT-XTS-AES len
let identity_token = &populated_buffer[..V0_IDENTITY_TOKEN_LEN];
self.metadata_key_hmac_key
.verify_hmac::<C>(identity_token, self.metadata_key_tag)
.map_err(|_| LdtAdvDecryptError::MacMismatch)?;
let token_arr: [u8; V0_IDENTITY_TOKEN_LEN] =
identity_token.try_into().expect("Length verified above");
Ok((
token_arr.into(),
ArrayView::try_from_slice(&buffer[V0_IDENTITY_TOKEN_LEN..payload.len()])
.expect("Buffer len less token len is the max output len"),
))
}
}
/// Errors that can occur during [AuthenticatedNpLdtDecryptCipher::decrypt_and_verify].
#[derive(Debug, PartialEq, Eq)]
pub enum LdtAdvDecryptError {
/// The ciphertext data was an invalid length.
InvalidLength(usize),
/// The MAC calculated from the plaintext did not match the expected value
MacMismatch,
}
impl fmt::Display for LdtAdvDecryptError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LdtAdvDecryptError::InvalidLength(len) => {
write!(f, "Adv decrypt error: invalid length ({len})")
}
LdtAdvDecryptError::MacMismatch => write!(f, "Adv decrypt error: MAC mismatch"),
}
}
}
/// Build a XorPadder by HKDFing the NP advertisement salt
pub fn salt_padder<C: CryptoProvider>(salt: V0Salt) -> XorPadder<BLOCK_SIZE> {
XorPadder::from(v0_ldt_expanded_salt::<C>(&salt.bytes))
}