| // 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)); |
| } |
| } |