blob: ba0b0777dbaf1075828c1041a96ddfcce19f25cf [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use core::num::NonZeroU8;
use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError};
use bt_common::{decodable_enum, Uuid};
use crate::crypto::set_identity_hash;
/// 16-bit UUID value for the Coordinated Set Identification Service and its
/// characteristics.
pub const COORDINATED_SET_IDENTIFICATION_SERVICE_UUID: Uuid = Uuid::from_u16(0x1846);
pub const SET_IDENTITY_RESOLVING_KEY_UUID: Uuid = Uuid::from_u16(0x2B84);
pub const COORDINATED_SET_SIZE_UUID: Uuid = Uuid::from_u16(0x2B85);
pub const SET_MEMBER_LOCK_UUID: Uuid = Uuid::from_u16(0x2B86);
pub const SET_MEMBER_RANK_UUID: Uuid = Uuid::from_u16(0x2B87);
// TODO(b/534436497): Add Coordinated Set Name characteristic (CSIS v1.1 Section
// 5.5).
// TODO(b/534436497): Add CsisApplicationError enum.
decodable_enum! {
/// The type of the Set Identity Resolving Key (SIRK).
/// See CSIS v1.1 Section 5.1.1, Table 5.2.
pub enum SirkType<u8, bt_common::packet_encoding::Error, OutOfRange> {
Encrypted = 0x00,
Plaintext = 0x01,
}
}
/// The Set Identity Resolving Key (SIRK) characteristic exposes the key
/// associated with the Coordinated Set (1 octet Type + 16 octets Value).
/// See CSIS v1.1 Section 5.1.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SetIdentityResolvingKey {
sirk_type: SirkType,
value: [u8; 16],
}
impl SetIdentityResolvingKey {
pub const BYTE_SIZE: usize = 17;
pub fn new(sirk_type: SirkType, value: [u8; 16]) -> Self {
Self { sirk_type, value }
}
pub fn sirk_type(&self) -> SirkType {
self.sirk_type
}
/// Resolves `rsi` with this SIRK. See CSIS v1.1 Section 4.9.
// TODO(b/534436497): Decrypt an encrypted SIRK with the LTK (CSIS v1.1
// Section 4.6).
pub fn resolve_rsi(&self, rsi: ResolvableSetIdentifier) -> RsiResolution {
if self.sirk_type == SirkType::Encrypted {
return RsiResolution::NeedsDecryption;
}
if set_identity_hash(&self.value, rsi.prand()) == *rsi.hash() {
RsiResolution::Resolved
} else {
RsiResolution::NotResolved
}
}
}
/// The outcome of resolving a Resolvable Set Identifier with a SIRK.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RsiResolution {
/// The RSI was generated from this SIRK.
Resolved,
/// The RSI was not generated from this SIRK.
NotResolved,
/// The SIRK is encrypted and has to be decrypted before it can resolve the
/// RSI.
NeedsDecryption,
}
/// The Resolvable Set Identifier that a Set Member advertises so that a Set
/// Coordinator holding the SIRK can recognize it without connecting.
///
/// Carried as `hash || prand`, least significant octet first, over six octets.
/// See CSIS v1.1 Section 4.8.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ResolvableSetIdentifier {
hash: [u8; 3],
prand: [u8; 3],
}
impl ResolvableSetIdentifier {
pub const BYTE_SIZE: usize = 6;
pub fn hash(&self) -> &[u8; 3] {
&self.hash
}
pub fn prand(&self) -> &[u8; 3] {
&self.prand
}
}
impl TryFrom<[u8; 6]> for ResolvableSetIdentifier {
type Error = PacketError;
/// Rejects `bytes` unless the two most significant bits of its prand are 0
/// and 1, as Core Spec v5.3 Vol 3, Part H, Section 1.3 requires.
fn try_from(bytes: [u8; 6]) -> Result<Self, Self::Error> {
if (bytes[5] & 0xc0) != 0x40 {
return Err(PacketError::InvalidParameter(format!(
"prand has invalid most significant bits: {:#04x}",
bytes[5]
)));
}
Ok(Self { hash: [bytes[0], bytes[1], bytes[2]], prand: [bytes[3], bytes[4], bytes[5]] })
}
}
/// Omits the key.
impl std::fmt::Debug for SetIdentityResolvingKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SetIdentityResolvingKey")
.field("sirk_type", &self.sirk_type)
.finish_non_exhaustive()
}
}
/// Omits the key.
impl std::fmt::Display for SetIdentityResolvingKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} SIRK", self.sirk_type)
}
}
impl Decodable for SetIdentityResolvingKey {
type Error = PacketError;
fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) {
if buf.len() < Self::BYTE_SIZE {
return (Err(PacketError::UnexpectedDataLength), buf.len());
}
let sirk_type = match SirkType::try_from(buf[0]) {
Ok(t) => t,
Err(e) => return (Err(e), Self::BYTE_SIZE),
};
let mut value = [0; 16];
value.copy_from_slice(&buf[1..17]);
(Ok(Self { sirk_type, value }), Self::BYTE_SIZE)
}
}
/// The Set Member Rank characteristic exposes a numeric value that is unique
/// within a Coordinated Set (0x01 to set size). See CSIS v1.1 Section 5.4.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SetMemberRank(NonZeroU8);
impl SetMemberRank {
pub const BYTE_SIZE: usize = 1;
pub fn new(rank: NonZeroU8) -> Self {
Self(rank)
}
pub fn value(&self) -> NonZeroU8 {
self.0
}
}
impl Decodable for SetMemberRank {
type Error = PacketError;
fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) {
if buf.is_empty() {
return (Err(PacketError::UnexpectedDataLength), 0);
}
let val = match NonZeroU8::new(buf[0]) {
Some(v) => v,
None => {
return (
Err(PacketError::InvalidParameter("SetMemberRank cannot be 0".to_string())),
1,
)
}
};
(Ok(Self(val)), Self::BYTE_SIZE)
}
}
impl Encodable for SetMemberRank {
type Error = PacketError;
fn encoded_len(&self) -> usize {
Self::BYTE_SIZE
}
fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
if buf.is_empty() {
return Err(PacketError::BufferTooSmall);
}
buf[0] = self.0.get();
Ok(())
}
}
/// The Coordinated Set Size characteristic exposes the number of devices
/// comprising the Coordinated Set (0x01 to 0xFF). See CSIS v1.1 Section 5.2.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CoordinatedSetSize(NonZeroU8);
impl CoordinatedSetSize {
pub const BYTE_SIZE: usize = 1;
pub fn new(size: NonZeroU8) -> Self {
Self(size)
}
pub fn value(&self) -> NonZeroU8 {
self.0
}
}
impl Decodable for CoordinatedSetSize {
type Error = PacketError;
fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) {
if buf.is_empty() {
return (Err(PacketError::UnexpectedDataLength), 0);
}
let val = match NonZeroU8::new(buf[0]) {
Some(v) => v,
None => {
return (
Err(PacketError::InvalidParameter(
"CoordinatedSetSize cannot be 0".to_string(),
)),
1,
);
}
};
(Ok(Self(val)), Self::BYTE_SIZE)
}
}
impl Encodable for CoordinatedSetSize {
type Error = PacketError;
fn encoded_len(&self) -> usize {
Self::BYTE_SIZE
}
fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
if buf.is_empty() {
return Err(PacketError::BufferTooSmall);
}
buf[0] = self.0.get();
Ok(())
}
}
decodable_enum! {
/// The Set Member Lock characteristic value. All other values are RFU.
/// See CSIS v1.1 Section 5.3, Table 5.4.
pub enum SetMemberLock<u8, bt_common::packet_encoding::Error, OutOfRange> {
Unlocked = 0x01,
Locked = 0x02,
}
}
impl SetMemberLock {
pub const BYTE_SIZE: usize = 1;
}
impl Decodable for SetMemberLock {
type Error = PacketError;
fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) {
if buf.is_empty() {
return (Err(PacketError::UnexpectedDataLength), 0);
}
let lock = match Self::try_from(buf[0]) {
Ok(val) => val,
Err(e) => return (Err(e), Self::BYTE_SIZE),
};
(Ok(lock), Self::BYTE_SIZE)
}
}
impl Encodable for SetMemberLock {
type Error = PacketError;
fn encoded_len(&self) -> usize {
Self::BYTE_SIZE
}
fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
if buf.is_empty() {
return Err(PacketError::BufferTooSmall);
}
buf[0] = (*self).into();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::test_vectors::{RSI, SIRK};
use assert_matches::assert_matches;
fn rsi(bytes: [u8; 6]) -> ResolvableSetIdentifier {
ResolvableSetIdentifier::try_from(bytes).expect("bytes are a well formed RSI")
}
#[test]
fn sirk_decoding() {
let mut buf = [0; 17];
buf[0] = 0x01; // Plaintext
buf[1..17].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let (res, consumed) = SetIdentityResolvingKey::decode(&buf);
assert_eq!(consumed, 17);
assert_eq!(
res.unwrap(),
SetIdentityResolvingKey::new(
SirkType::Plaintext,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
)
);
}
#[test]
fn plaintext_sirk_resolves_its_own_rsi() {
let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SIRK);
assert_eq!(sirk.resolve_rsi(rsi(RSI)), RsiResolution::Resolved);
let mut foreign = RSI;
foreign[0] ^= 0xff;
assert_eq!(sirk.resolve_rsi(rsi(foreign)), RsiResolution::NotResolved);
}
#[test]
fn encrypted_sirk_needs_decryption() {
let sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, SIRK);
assert_eq!(sirk.resolve_rsi(rsi(RSI)), RsiResolution::NeedsDecryption);
}
#[test]
fn rsi_splits_into_hash_and_prand() {
let rsi = rsi(RSI);
assert_eq!(rsi.hash(), &[RSI[0], RSI[1], RSI[2]]);
assert_eq!(rsi.prand(), &[RSI[3], RSI[4], RSI[5]]);
}
#[test]
fn rsi_with_malformed_prand_is_rejected() {
// The two most significant bits of prand must be 0 and 1.
for msbs in [0x00, 0x80, 0xc0] {
let mut bytes = RSI;
bytes[5] = msbs | (RSI[5] & 0x3f);
assert_matches!(
ResolvableSetIdentifier::try_from(bytes),
Err(PacketError::InvalidParameter(_)),
"prand MSBs {msbs:#04x} should be rejected"
);
}
}
#[test]
fn set_member_rank_decoding() {
let mut buf = [0; 1];
buf[0] = 0x05;
let (res, consumed) = SetMemberRank::decode(&buf);
assert_eq!(consumed, 1);
assert_eq!(res.unwrap().value().get(), 5);
buf[0] = 0x00; // Invalid
let (res, _) = SetMemberRank::decode(&buf);
assert_matches!(res, Err(PacketError::InvalidParameter(_)));
}
#[test]
fn set_member_rank_encoding() {
let rank = SetMemberRank::new(NonZeroU8::new(5).unwrap());
assert_eq!(rank.encoded_len(), 1);
let mut buf = [0; 1];
rank.encode(&mut buf).unwrap();
assert_eq!(buf[0], 0x05);
let mut empty_buf = [];
assert_matches!(rank.encode(&mut empty_buf), Err(PacketError::BufferTooSmall));
}
#[test]
fn coordinated_set_size_decoding() {
let mut buf = [0; 1];
buf[0] = 0x02;
let (res, consumed) = CoordinatedSetSize::decode(&buf);
assert_eq!(consumed, 1);
assert_eq!(res.unwrap().value().get(), 2);
buf[0] = 0x00; // Invalid
let (res, _) = CoordinatedSetSize::decode(&buf);
assert_matches!(res, Err(PacketError::InvalidParameter(_)));
}
#[test]
fn coordinated_set_size_encoding() {
let size = CoordinatedSetSize::new(NonZeroU8::new(2).unwrap());
assert_eq!(size.encoded_len(), 1);
let mut buf = [0; 1];
size.encode(&mut buf).unwrap();
assert_eq!(buf[0], 0x02);
let mut empty_buf = [];
assert_matches!(size.encode(&mut empty_buf), Err(PacketError::BufferTooSmall));
}
#[test]
fn set_member_lock_decoding() {
let (res, consumed) = SetMemberLock::decode(&[0x01]);
assert_eq!(consumed, 1);
assert_eq!(res.unwrap(), SetMemberLock::Unlocked);
let (res, consumed) = SetMemberLock::decode(&[0x02]);
assert_eq!(consumed, 1);
assert_eq!(res.unwrap(), SetMemberLock::Locked);
// 0x00 and anything above 0x02 are RFU per CSIS v1.1 Section 5.3.
let (res, _) = SetMemberLock::decode(&[0x00]);
assert_matches!(res, Err(_));
let (res, _) = SetMemberLock::decode(&[0x03]);
assert_matches!(res, Err(_));
let (res, consumed) = SetMemberLock::decode(&[]);
assert_eq!(consumed, 0);
assert_matches!(res, Err(PacketError::UnexpectedDataLength));
}
#[test]
fn set_member_lock_encoding() {
let lock = SetMemberLock::Locked;
assert_eq!(lock.encoded_len(), 1);
let mut buf = [0; 1];
lock.encode(&mut buf).unwrap();
assert_eq!(buf[0], 0x02);
SetMemberLock::Unlocked.encode(&mut buf).unwrap();
assert_eq!(buf[0], 0x01);
let mut empty_buf = [];
assert_matches!(lock.encode(&mut empty_buf), Err(PacketError::BufferTooSmall));
}
}