blob: 31aa50d736d6b7febe6233f2ba79b4d76d2d6a29 [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]
#![forbid(unsafe_code)]
#![deny(
missing_docs,
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::panic,
clippy::expect_used
)]
#[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 crypto_provider::{aes::BLOCK_SIZE, CryptoProvider};
use ldt::{LdtDecryptCipher, LdtEncryptCipher, LdtError, Mix, Padder, Swap, XorPadder};
use ldt_tbc::TweakableBlockCipher;
use np_hkdf::{legacy_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;
/// Legacy (v0) format uses a 14-byte metadata key
pub const NP_LEGACY_METADATA_KEY_LEN: usize = 14;
/// The salt included in an 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 LegacySalt {
/// Salt bytes extracted from the incoming NP advertisement
bytes: [u8; 2],
}
impl LegacySalt {
/// Returns the salt as a byte array.
pub fn bytes(&self) -> &[u8; 2] {
&self.bytes
}
}
impl From<[u8; 2]> for LegacySalt {
fn from(arr: [u8; 2]) -> Self {
Self { bytes: arr }
}
}
/// [LdtEncryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
pub type LdtEncrypterXtsAes128<C> = LdtEncryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
/// A Nearby Presence specific LDT decrypter which verifies the hmac tag of the given payload
/// parameterized for XTS-AES-128 with the [Swap] mix function.
pub type LdtNpAdvDecrypterXtsAes128<C> =
LdtNpAdvDecrypter<{ BLOCK_SIZE }, LDT_XTS_AES_MAX_LEN, XtsAes128<C>, Swap, C>;
/// 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>,
metadata_key_tag: [u8; 32],
) -> LdtNpAdvDecrypterXtsAes128<C> {
build_np_adv_decrypter(
&key_seed.legacy_ldt_key(),
metadata_key_tag,
key_seed.legacy_metadata_key_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>,
metadata_key_tag: [u8; 32],
metadata_key_hmac_key: NpHmacSha256Key<C>,
) -> LdtNpAdvDecrypterXtsAes128<C> {
LdtNpAdvDecrypter {
ldt_decrypter: LdtXtsAes128Decrypter::<C>::new(ldt_key),
metadata_key_tag,
metadata_key_hmac_key,
}
}
// [LdtDecryptCipher] parameterized for XTS-AES-128 with the [Swap] mix function.
type LdtXtsAes128Decrypter<C> = LdtDecryptCipher<{ BLOCK_SIZE }, XtsAes128<C>, Swap>;
/// 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) | Identity DE header (1 byte) | Salt (2 bytes) | Identity (14 bytes) | repeated
/// { DE header | DE payload }
///
/// Example:
/// Header (1 byte) | Identity DE header (1 byte) | Salt (2 bytes) | Identity (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 LdtNpAdvDecrypter<
const B: usize,
const O: usize,
T: TweakableBlockCipher<B>,
M: Mix,
C: CryptoProvider,
> {
ldt_decrypter: LdtDecryptCipher<B, T, M>,
metadata_key_tag: [u8; 32],
metadata_key_hmac_key: NpHmacSha256Key<C>,
}
impl<const B: usize, const O: usize, T, M, C> LdtNpAdvDecrypter<B, O, T, M, C>
where
T: TweakableBlockCipher<B>,
M: Mix,
C: CryptoProvider,
{
/// Decrypt an advertisement payload using the provided padder.
///
/// If the plaintext's metadata key matches this item's MAC, return the plaintext, otherwise `None`.
///
/// 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 of `[B, B * 2)`.
/// - If the decrypted plaintext fails its HMAC validation
pub fn decrypt_and_verify<P: Padder<B, T>>(
&self,
payload: &[u8],
padder: &P,
) -> Result<ArrayView<u8, O>, LdtAdvDecryptError> {
assert_eq!(B * 2 - 1, O); // should be compiled away
// have to check length before passing to LDT to ensure copying into the buffer is safe
if payload.len() < B || payload.len() > O {
return Err(LdtAdvDecryptError::InvalidLength(payload.len()));
}
// we copy to avoid exposing plaintext that hasn't been validated w/ hmac
let mut buffer = [0_u8; O];
buffer[..payload.len()].copy_from_slice(payload);
#[allow(clippy::expect_used)]
self.ldt_decrypter
.decrypt(&mut buffer[..payload.len()], padder)
.map_err(|e| match e {
LdtError::InvalidLength(l) => LdtAdvDecryptError::InvalidLength(l),
})
.and_then(|_| {
self.metadata_key_hmac_key
.verify_hmac(&buffer[..NP_LEGACY_METADATA_KEY_LEN], self.metadata_key_tag)
.map_err(|_| LdtAdvDecryptError::MacMismatch)
.map(|_| {
ArrayView::try_from_array(buffer, payload.len())
.expect("this will never be hit because the length is validated above")
})
})
}
}
/// Errors that can occur during [LdtNpAdvDecrypter::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<const B: usize, C: CryptoProvider>(salt: LegacySalt) -> XorPadder<{ B }> {
// Assuming that the tweak size == the block size here, which it is for XTS.
// If that's ever not true, yet another generic parameter will address that.
XorPadder::from(legacy_ldt_expanded_salt::<B, C>(&salt.bytes))
}