blob: a6a155e09e169b43f121b4b1c1b2bf366116b58d [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.
//! Serialization and deserialization for v0 (legacy) and v1 (extended) Nearby Presence
//! advertisements.
//!
//! See `tests/examples_v0.rs` and `tests/examples_v1.rs` for some tests that show common
//! deserialization scenarios.
#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
extern crate alloc;
extern crate core;
use crate::{
credential::{
source::{BothCredentialSource, CredentialSource},
v0::V0CryptoMaterial,
MatchedCredFromCred, MatchedCredential, V0Credential, V1Credential,
},
extended::deserialize::{
parse_sections, CiphertextSection, DataElements, DecryptedSection, IntermediateSection,
PlaintextSection, Section, SectionDeserializeError,
},
legacy::deserialize::{
DecryptError, DecryptedAdvContents, IntermediateAdvContents, PlaintextAdvContents,
},
};
use alloc::vec::Vec;
#[cfg(feature = "devtools")]
use array_view::ArrayView;
use core::{fmt::Debug, marker};
use crypto_provider::CryptoProvider;
#[cfg(feature = "devtools")]
use extended::NP_ADV_MAX_SECTION_LEN;
use legacy::{data_elements::DataElementDeserializeError, deserialize::AdvDeserializeError};
use nom::{combinator, number};
pub mod credential;
pub mod de_type;
#[cfg(test)]
mod deser_v0_tests;
#[cfg(test)]
mod deser_v1_tests;
pub mod extended;
#[cfg(test)]
mod header_parse_tests;
pub mod legacy;
pub mod shared_data;
/// Canonical form of NP's service UUID.
///
/// Note that UUIDs are encoded in BT frames in little-endian order, so these bytes may need to be
/// reversed depending on the host BT API.
pub const NP_SVC_UUID: [u8; 2] = [0xFC, 0xF1];
/// Parse, deserialize, decrypt, and validate a complete NP advertisement (the entire contents of
/// the service data for the NP UUID).
pub fn deserialize_advertisement<'s, C0, C1, M, S, P>(
adv: &[u8],
cred_source: &'s S,
) -> Result<DeserializedAdvertisement<'s, M>, AdvDeserializationError>
where
C0: V0Credential<Matched<'s> = M> + 's,
C1: V1Credential<Matched<'s> = M> + 's,
M: MatchedCredential<'s>,
S: BothCredentialSource<C0, C1>,
P: CryptoProvider,
{
let (remaining, header) =
parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
match header {
AdvHeader::V1(header) => {
deser_decrypt_v1::<C1, S::V1Source, P>(cred_source.v1(), remaining, header)
.map(DeserializedAdvertisement::V1)
}
AdvHeader::V0 => deser_decrypt_v0::<C0, S::V0Source, P>(cred_source.v0(), remaining)
.map(DeserializedAdvertisement::V0),
}
}
/// Parse, deserialize, decrypt, and validate a complete V0 NP advertisement (the entire contents
/// of the service data for the NP UUID). If the advertisement version header does not match V0,
/// this method will return an [`AdvDeserializationError::HeaderParseError`]
pub fn deserialize_v0_advertisement<'s, C, S, P>(
adv: &[u8],
cred_source: &'s S,
) -> Result<V0AdvertisementContents<'s, C>, AdvDeserializationError>
where
C: V0Credential,
S: CredentialSource<C>,
P: CryptoProvider,
{
let (remaining, header) =
parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
match header {
AdvHeader::V0 => deser_decrypt_v0::<C, S, P>(cred_source, remaining),
AdvHeader::V1(_) => Err(AdvDeserializationError::HeaderParseError),
}
}
/// Parse, deserialize, decrypt, and validate a complete V1 NP advertisement (the entire contents
/// of the service data for the NP UUID). If the advertisement version header does not match V1,
/// this method will return an [`AdvDeserializationError::HeaderParseError`]
pub fn deserialize_v1_advertisement<'s, C, S, P>(
adv: &[u8],
cred_source: &'s S,
) -> Result<V1AdvertisementContents<'s, C>, AdvDeserializationError>
where
C: V1Credential,
S: CredentialSource<C>,
P: CryptoProvider,
{
let (remaining, header) =
parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
match header {
AdvHeader::V0 => Err(AdvDeserializationError::HeaderParseError),
AdvHeader::V1(header) => deser_decrypt_v1::<C, S, P>(cred_source, remaining, header),
}
}
type V1AdvertisementContents<'s, C> = V1AdvContents<'s, MatchedCredFromCred<'s, C>>;
/// The encryption scheme used for a V1 advertisement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum V1EncryptionScheme {
/// Indicates MIC-based encryption and verification.
Mic,
/// Indicates signature-based encryption and verification.
Signature,
}
/// Error in decryption operations for `deser_decrypt_v1_section_bytes_for_dev_tools`.
#[cfg(feature = "devtools")]
#[derive(Debug, Clone)]
pub enum AdvDecryptionError {
/// Cannot decrypt because the input section is not encrypted.
InputNotEncrypted,
/// Error parsing the given section.
ParseError,
/// No suitable credential found to decrypt the given section.
NoMatchingCredentials,
}
/// Decrypt, but do not further deserialize the v1 bytes, intended for developer tooling uses only.
/// Production uses should use [deserialize_v1_advertisement] instead, which deserializes to a
/// structured format and provides extra type safety.
#[cfg(feature = "devtools")]
pub fn deser_decrypt_v1_section_bytes_for_dev_tools<S, V1, P>(
cred_source: &S,
header_byte: u8,
section_bytes: &[u8],
) -> Result<(ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, V1EncryptionScheme), AdvDecryptionError>
where
S: CredentialSource<V1>,
V1: V1Credential,
P: CryptoProvider,
{
let header = V1Header { header_byte };
let int_sections =
parse_sections(&header, section_bytes).map_err(|_| AdvDecryptionError::ParseError)?;
let cipher_section = match &int_sections[0] {
IntermediateSection::Plaintext(_) => Err(AdvDecryptionError::InputNotEncrypted)?,
IntermediateSection::Ciphertext(section) => section,
};
use crate::credential::v1::V1CryptoMaterial;
use core::borrow::Borrow;
for cred in cred_source.iter() {
let crypto_material = cred.crypto_material();
match cipher_section {
CiphertextSection::SignatureEncryptedIdentity(encrypted_section) => {
let identity_resolution_material =
crypto_material.signed_identity_resolution_material::<P>();
match encrypted_section.try_decrypt::<P>(identity_resolution_material.borrow()) {
Ok(plaintext) => return Ok((plaintext, V1EncryptionScheme::Signature)),
Err(_) => continue,
}
}
CiphertextSection::MicEncryptedIdentity(encrypted_section) => {
let identity_resolution_material =
crypto_material.unsigned_identity_resolution_material::<P>();
let verification_material = crypto_material.unsigned_verification_material::<P>();
match encrypted_section
.try_decrypt::<P>(identity_resolution_material.borrow(), &verification_material)
{
Ok(plaintext) => return Ok((plaintext, V1EncryptionScheme::Mic)),
Err(_) => continue,
}
}
}
}
Err(AdvDecryptionError::NoMatchingCredentials)
}
/// Deserialize and decrypt the contents of a v1 adv after the version header
fn deser_decrypt_v1<'s, C, S, P>(
cred_source: &'s S,
remaining: &[u8],
header: V1Header,
) -> Result<V1AdvertisementContents<'s, C>, AdvDeserializationError>
where
C: V1Credential,
S: CredentialSource<C>,
P: CryptoProvider,
{
let int_sections =
parse_sections(&header, remaining).map_err(|_| AdvDeserializationError::ParseError {
details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError,
})?;
let mut sections = Vec::new();
let mut to_decrypt = Vec::new();
// keep track of ordering for later sorting
for (idx, s) in int_sections.into_iter().enumerate() {
match s {
IntermediateSection::Plaintext(p) => {
sections.push((idx, V1DeserializedSection::Plaintext(p)))
}
IntermediateSection::Ciphertext(c) => to_decrypt.push((idx, c)),
}
}
let mut invalid_sections = 0;
// Hot loop
// We assume that iterating credentials is more expensive than iterating sections
for cred in cred_source.iter() {
let mut i = 0;
while i < to_decrypt.len() {
let (section_idx, c): &(usize, CiphertextSection) = &to_decrypt[i];
match c.try_deserialize::<C, P>(cred) {
Ok(s) => {
sections.push((
*section_idx,
V1DeserializedSection::Decrypted(WithMatchedCredential::new(
cred.matched(),
s,
)),
));
// we don't care about maintaining order, so use O(1) remove
to_decrypt.swap_remove(i);
// don't advance i -- it now points to a new element
}
Err(e) => match e {
SectionDeserializeError::IncorrectCredential => {
// keep it around to try with another credential
i += 1;
}
SectionDeserializeError::ParseError => {
// the credential worked, but the section itself was bogus, so drop
// it
invalid_sections += 1;
to_decrypt.swap_remove(i);
}
},
}
}
if to_decrypt.is_empty() {
// no need to consider the remaining credentials
break;
}
}
invalid_sections += to_decrypt.len();
// decryption may produce sections out of order
sections.sort_by_key(|(idx, _section)| *idx);
Ok(V1AdvContents::new(sections.into_iter().map(|(_idx, s)| s).collect(), invalid_sections))
}
type V0AdvertisementContents<'s, C> = V0AdvContents<'s, MatchedCredFromCred<'s, C>>;
/// Deserialize and decrypt the contents of a v0 adv after the version header
fn deser_decrypt_v0<'s, C, S, P>(
cred_source: &'s S,
remaining: &[u8],
) -> Result<V0AdvertisementContents<'s, C>, AdvDeserializationError>
where
C: V0Credential,
S: CredentialSource<C>,
P: CryptoProvider,
{
let contents = legacy::deserialize::deserialize_adv_contents::<P>(remaining)?;
return match contents {
IntermediateAdvContents::Plaintext(p) => Ok(V0AdvContents::Plaintext(p)),
IntermediateAdvContents::Ciphertext(c) => {
for cred in cred_source.iter() {
let cm = cred.crypto_material();
let ldt = cm.ldt_adv_cipher::<P>();
match c.try_decrypt(&ldt) {
Ok(c) => {
return Ok(V0AdvContents::Decrypted(WithMatchedCredential::new(
cred.matched(),
c,
)))
}
Err(e) => match e {
DecryptError::DecryptOrVerifyError => continue,
DecryptError::DeserializeError(e) => {
return Err(e.into());
}
},
}
}
Ok(V0AdvContents::NoMatchingCredentials)
}
};
}
/// Parse a NP advertisement header.
///
/// This can be used on all versions of advertisements since it's the header that determines the
/// version.
///
/// Returns a `nom::IResult` with the parsed header and the remaining bytes of the advertisement.
fn parse_adv_header(adv: &[u8]) -> nom::IResult<&[u8], AdvHeader> {
// header bits: VVVxxxxx
let (remaining, (header_byte, version, _low_bits)) = combinator::verify(
// splitting a byte at a bit boundary to take lower 5 bits
combinator::map(number::complete::u8, |byte| (byte, byte >> 5, byte & 0x1F)),
|&(_header_byte, version, low_bits)| match version {
// reserved bits, for any version, must be zero
PROTOCOL_VERSION_LEGACY | PROTOCOL_VERSION_EXTENDED => low_bits == 0,
_ => false,
},
)(adv)?;
match version {
PROTOCOL_VERSION_LEGACY => Ok((remaining, AdvHeader::V0)),
PROTOCOL_VERSION_EXTENDED => Ok((remaining, AdvHeader::V1(V1Header { header_byte }))),
_ => unreachable!(),
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum AdvHeader {
V0,
V1(V1Header),
}
/// An NP advertisement with its header parsed.
#[derive(Debug, PartialEq, Eq)]
pub enum DeserializedAdvertisement<'m, M: MatchedCredential<'m>> {
/// V0 header has all reserved bits, so there is no data to represent other than the version
/// itself.
V0(V0AdvContents<'m, M>),
/// V1 advertisement
V1(V1AdvContents<'m, M>),
}
/// The contents of a deserialized and decrypted V1 advertisement.
#[derive(Debug, PartialEq, Eq)]
pub struct V1AdvContents<'m, M: MatchedCredential<'m>> {
sections: Vec<V1DeserializedSection<'m, M>>,
invalid_sections: usize,
}
impl<'m, M: MatchedCredential<'m>> V1AdvContents<'m, M> {
fn new(sections: Vec<V1DeserializedSection<'m, M>>, invalid_sections: usize) -> Self {
Self { sections, invalid_sections }
}
/// Destructures this V1 advertisement into just the sections
/// which could be successfully deserialized and decrypted
pub fn into_valid_sections(self) -> Vec<V1DeserializedSection<'m, M>> {
self.sections
}
/// The sections that could be successfully deserialized and decrypted
pub fn sections(&self) -> impl Iterator<Item = &V1DeserializedSection<M>> {
self.sections.iter()
}
/// The number of sections that could not be parsed or decrypted.
pub fn invalid_sections_count(&self) -> usize {
self.invalid_sections
}
}
/// Advertisement content that was either already plaintext or has been decrypted.
#[derive(Debug, PartialEq, Eq)]
pub enum V0AdvContents<'m, M: MatchedCredential<'m>> {
/// Contents of an originally plaintext advertisement
Plaintext(PlaintextAdvContents),
/// Contents that was ciphertext in the original advertisement, and has been decrypted
/// with the credential in the [MatchedCredential]
Decrypted(WithMatchedCredential<'m, M, DecryptedAdvContents>),
/// The advertisement was encrypted, but no credentials matched
NoMatchingCredentials,
}
/// Advertisement content that was either already plaintext or has been decrypted.
#[derive(Debug, PartialEq, Eq)]
pub enum V1DeserializedSection<'m, M: MatchedCredential<'m>> {
/// Section that was plaintext in the original advertisement
Plaintext(PlaintextSection),
/// Section that was ciphertext in the original advertisement, and has been decrypted
/// with the credential in the [MatchedCredential]
Decrypted(WithMatchedCredential<'m, M, DecryptedSection>),
}
impl<'m, M> Section for V1DeserializedSection<'m, M>
where
M: MatchedCredential<'m>,
{
type Iterator<'d> = DataElements<'d> where Self: 'd;
fn data_elements(&'_ self) -> Self::Iterator<'_> {
match self {
V1DeserializedSection::Plaintext(p) => p.data_elements(),
V1DeserializedSection::Decrypted(d) => d.contents.data_elements(),
}
}
}
/// Decrypted advertisement content with the [MatchedCredential] from the credential that decrypted
/// it.
#[derive(Debug, PartialEq, Eq)]
pub struct WithMatchedCredential<'m, M: MatchedCredential<'m>, T> {
matched: M,
contents: T,
// the compiler sees 'm as unused
marker: marker::PhantomData<&'m ()>,
}
impl<'m, M: MatchedCredential<'m>, T> WithMatchedCredential<'m, M, T> {
fn new(matched: M, contents: T) -> Self {
Self { matched, contents, marker: marker::PhantomData }
}
/// Credential data for the credential that decrypted the content.
pub fn matched_credential(&self) -> &M {
&self.matched
}
/// The decrypted advertisement content.
pub fn contents(&self) -> &T {
&self.contents
}
}
/// Data in a V1 advertisement header.
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) struct V1Header {
header_byte: u8,
}
const PROTOCOL_VERSION_LEGACY: u8 = 0;
const PROTOCOL_VERSION_EXTENDED: u8 = 1;
/// Errors that can occur during advertisement deserialization.
#[derive(PartialEq)]
pub enum AdvDeserializationError {
/// The advertisement header could not be parsed
HeaderParseError,
/// The advertisement content could not be parsed
ParseError {
/// Potentially hazardous details about deserialization errors. Read the documentation for
/// [AdvDeserializationErrorDetailsHazmat] before using this field.
details_hazmat: AdvDeserializationErrorDetailsHazmat,
},
}
impl Debug for AdvDeserializationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AdvDeserializationError::HeaderParseError => write!(f, "HeaderParseError"),
AdvDeserializationError::ParseError { .. } => write!(f, "ParseError"),
}
}
}
/// Potentially hazardous details about deserialization errors. These error information can
/// potentially expose side-channel information about the plaintext of the advertisements and/or
/// the keys used to decrypt them. For any place that you avoid exposing the keys directly
/// (e.g. across FFIs, print to log, etc), avoid exposing these error details as well.
#[derive(PartialEq)]
pub enum AdvDeserializationErrorDetailsHazmat {
/// Parsing the overall advertisement or DE structure failed
AdvertisementDeserializeError,
/// Deserializing an individual DE from its DE contents failed
V0DataElementDeserializeError(DataElementDeserializeError),
/// Must not have any other top level data elements if there is an encrypted identity DE
TooManyTopLevelDataElements,
/// Must not have an identity DE inside an identity DE
InvalidDataElementHierarchy,
}
impl From<AdvDeserializeError> for AdvDeserializationError {
fn from(err: AdvDeserializeError) -> Self {
match err {
AdvDeserializeError::AdvertisementDeserializeError => {
AdvDeserializationError::ParseError {
details_hazmat:
AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError,
}
}
AdvDeserializeError::DataElementDeserializeError(e) => {
AdvDeserializationError::ParseError {
details_hazmat:
AdvDeserializationErrorDetailsHazmat::V0DataElementDeserializeError(e),
}
}
AdvDeserializeError::TooManyTopLevelDataElements => {
AdvDeserializationError::ParseError {
details_hazmat:
AdvDeserializationErrorDetailsHazmat::TooManyTopLevelDataElements,
}
}
AdvDeserializeError::InvalidDataElementHierarchy => {
AdvDeserializationError::ParseError {
details_hazmat:
AdvDeserializationErrorDetailsHazmat::InvalidDataElementHierarchy,
}
}
}
}
}
/// DE length is out of range (e.g. > 4 bits for encoded V0, > max DE size for actual V0, >127 for
/// V1) or invalid for the relevant DE type.
#[derive(Debug, PartialEq, Eq)]
pub struct DeLengthOutOfRange;
/// The identity mode for a deserialized plaintext section or advertisement.
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum PlaintextIdentityMode {
/// No identity DE was present in the section
None,
/// A "Public Identity" DE was present in the section
Public,
}
/// A "public identity" -- a nonspecific "empty identity".
///
/// Used when serializing V0 advertisements or V1 sections.
#[derive(Default, Debug)]
pub struct PublicIdentity {}
/// The lack of any identity information whatsoever, which is distinct from [PublicIdentity].
///
/// Used when serializing V0 advertisements or V1 sections.
#[derive(Default, Debug)]
pub struct NoIdentity {}