rust/bt-csis: Resolve Resolvable Set Identifiers

Resolve an RSI against a SIRK with the sih hash. Drop the key accessor
and Encodable impl now that nothing outside the module needs the bytes.

Bug: 534436497
Test: cargo test
Change-Id: Id5e6ffdcc09997db3b300b2d73eb36797e626ebb
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/4161
diff --git a/rust/bt-csis/Cargo.toml b/rust/bt-csis/Cargo.toml
index 54b3d09..62e32b3 100644
--- a/rust/bt-csis/Cargo.toml
+++ b/rust/bt-csis/Cargo.toml
@@ -5,6 +5,7 @@
 license.workspace = true
 
 [dependencies]
+aes.workspace = true
 bt-common.workspace = true
 bt-gatt.workspace = true
 futures.workspace = true
diff --git a/rust/bt-csis/src/client/event.rs b/rust/bt-csis/src/client/event.rs
index 99095bf..8809190 100644
--- a/rust/bt-csis/src/client/event.rs
+++ b/rust/bt-csis/src/client/event.rs
@@ -242,7 +242,7 @@
         let mut stream = client.take_notification_stream().expect("stream available");
 
         // Values read during discovery are visible before any notification.
-        assert_eq!(client.sirk().value(), &[0xAA; 16]);
+        assert_eq!(client.sirk(), SetIdentityResolvingKey::new(SirkType::Plaintext, [0xAA; 16]));
         assert_eq!(client.size(), Some(CoordinatedSetSize::new(NonZeroU8::new(2).unwrap())));
         assert_eq!(client.lock_state(), Some(SetMemberLock::Unlocked));
 
@@ -299,7 +299,7 @@
                 [0xBB; 16]
             ))
         );
-        assert_eq!(client.sirk().value(), &[0xBB; 16]);
+        assert_eq!(client.sirk(), SetIdentityResolvingKey::new(SirkType::Plaintext, [0xBB; 16]));
     }
 
     #[test]
diff --git a/rust/bt-csis/src/crypto.rs b/rust/bt-csis/src/crypto.rs
new file mode 100644
index 0000000..ded25aa
--- /dev/null
+++ b/rust/bt-csis/src/crypto.rs
@@ -0,0 +1,66 @@
+// 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 aes::cipher::{BlockCipherEncrypt, KeyInit};
+
+/// Set Identity Hash (SIH) function as defined in CSIS 1.1 Specification,
+/// Section 4.7. `sih(k, r) = e(k, r') mod 2^24`
+/// where `r' = padding || r`, padding is 104 bits of 0.
+/// The LSO of r becomes the LSO of r'.
+pub fn set_identity_hash(key: &[u8; 16], r: &[u8; 3]) -> [u8; 3] {
+    let mut plaintext = [0u8; 16];
+
+    // r' = padding || r. LSO of r is LSO of r'.
+    // Since Bluetooth is little endian, LSO is at index 0.
+    plaintext[0] = r[0];
+    plaintext[1] = r[1];
+    plaintext[2] = r[2];
+    // The rest of the bytes are already 0 (padding).
+
+    // Bluetooth `e` function uses AES-128 but is LSB-first (little endian).
+    // The `aes` crate is MSB-first. We need to reverse the input and output.
+    let mut key_reversed = *key;
+    key_reversed.reverse();
+    plaintext.reverse();
+
+    let key_block = key_reversed.into();
+    let mut block = plaintext.into();
+
+    let cipher = aes::Aes128::new(&key_block);
+    cipher.encrypt_block(&mut block);
+
+    // sih keeps the least significant 24 bits of e, the last three octets of
+    // the MSB-first block, returned LSO first.
+    [block[15], block[14], block[13]]
+}
+
+/// Sample values from the CSIS v1.1 specification, least significant octet
+/// first.
+#[cfg(test)]
+pub(crate) mod test_vectors {
+    /// Appendix A.1, `k`.
+    pub(crate) const SIRK: [u8; 16] = [
+        0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d,
+        0x45,
+    ];
+    /// Appendix A.1, `r`. In MSB-first notation, 0x69f563.
+    pub(crate) const PRAND: [u8; 3] = [0x63, 0xf5, 0x69];
+    /// Appendix A.1, `sih(k, r)`. In MSB-first notation, 0x1948da.
+    pub(crate) const HASH: [u8; 3] = [0xda, 0x48, 0x19];
+
+    /// The RSI that a Set Member holding `SIRK` advertises for `PRAND`, which
+    /// is `hash || prand`.
+    pub(crate) const RSI: [u8; 6] = [HASH[0], HASH[1], HASH[2], PRAND[0], PRAND[1], PRAND[2]];
+}
+
+#[cfg(test)]
+mod tests {
+    use super::test_vectors::{HASH, PRAND, SIRK};
+    use super::*;
+
+    #[test]
+    fn set_identity_hash_matches_the_spec_vector() {
+        assert_eq!(set_identity_hash(&SIRK, &PRAND), HASH);
+    }
+}
diff --git a/rust/bt-csis/src/lib.rs b/rust/bt-csis/src/lib.rs
index f5ee452..a7bf80d 100644
--- a/rust/bt-csis/src/lib.rs
+++ b/rust/bt-csis/src/lib.rs
@@ -3,4 +3,5 @@
 // found in the LICENSE file.
 
 pub mod client;
+pub mod crypto;
 pub mod types;
diff --git a/rust/bt-csis/src/types.rs b/rust/bt-csis/src/types.rs
index 04ef0d0..ba0b077 100644
--- a/rust/bt-csis/src/types.rs
+++ b/rust/bt-csis/src/types.rs
@@ -7,6 +7,8 @@
 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);
@@ -48,8 +50,71 @@
         self.sirk_type
     }
 
-    pub fn value(&self) -> &[u8; 16] {
-        &self.value
+    /// 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]] })
     }
 }
 
@@ -87,23 +152,6 @@
     }
 }
 
-impl Encodable for SetIdentityResolvingKey {
-    type Error = PacketError;
-
-    fn encoded_len(&self) -> usize {
-        Self::BYTE_SIZE
-    }
-
-    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
-        if buf.len() < Self::BYTE_SIZE {
-            return Err(PacketError::BufferTooSmall);
-        }
-        buf[0] = self.sirk_type.into();
-        buf[1..17].copy_from_slice(&self.value);
-        Ok(())
-    }
-}
-
 /// 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)]
@@ -259,8 +307,13 @@
 #[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];
@@ -269,18 +322,51 @@
 
         let (res, consumed) = SetIdentityResolvingKey::decode(&buf);
         assert_eq!(consumed, 17);
-        let sirk = res.unwrap();
-        assert_eq!(sirk.sirk_type(), SirkType::Plaintext);
-        assert_eq!(sirk.value(), &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+        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 sirk_encoding() {
-        let sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, [1; 16]);
-        let mut buf = [0; 17];
-        sirk.encode(&mut buf).unwrap();
-        assert_eq!(buf[0], 0x00);
-        assert_eq!(buf[1..17], [1; 16]);
+    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]
diff --git a/rust/bt-set-coordinator/Cargo.toml b/rust/bt-set-coordinator/Cargo.toml
index ad3d9f8..3ba82e4 100644
--- a/rust/bt-set-coordinator/Cargo.toml
+++ b/rust/bt-set-coordinator/Cargo.toml
@@ -9,4 +9,3 @@
 bt-common.workspace = true
 bt-gatt.workspace = true
 thiserror.workspace = true
-aes.workspace = true
diff --git a/rust/bt-set-coordinator/src/types.rs b/rust/bt-set-coordinator/src/types.rs
index 329bf35..5ca8169 100644
--- a/rust/bt-set-coordinator/src/types.rs
+++ b/rust/bt-set-coordinator/src/types.rs
@@ -2,8 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use aes::cipher::{BlockCipherEncrypt, KeyInit};
-use bt_csis::types::{SetIdentityResolvingKey, SirkType};
+use bt_csis::types::{ResolvableSetIdentifier, RsiResolution, SetIdentityResolvingKey};
 use bt_gatt::central::{AdvertisingDatum, ScanResult};
 
 /// Represents a Coordinated Set managed by the Set Coordinator.
@@ -21,125 +20,38 @@
     /// RSI (in standalone ResolvableSetIdentifier AD Type) and attempting to
     /// resolve it with this set's SIRK.
     pub fn matches_scan_result(&self, scan_result: &ScanResult) -> bool {
-        scan_result.advertised.iter().any(|datum| match datum {
-            AdvertisingDatum::ResolvableSetIdentifier(rsi) => self.resolves_rsi(rsi),
-            _ => false,
-        })
+        scan_result
+            .advertised
+            .iter()
+            .filter_map(|datum| match datum {
+                AdvertisingDatum::ResolvableSetIdentifier(bytes) => {
+                    ResolvableSetIdentifier::try_from(*bytes).ok()
+                }
+                _ => None,
+            })
+            .any(|rsi| self.sirk.resolve_rsi(rsi) == RsiResolution::Resolved)
     }
-
-    /// Resolves an RSI using the Set Identity Resolving Key (SIRK).
-    ///
-    /// RSI format (6 bytes):
-    /// bytes 0..3: hash
-    /// bytes 3..6: prand
-    ///
-    /// See CSIP v1.1 Section 4.8 & Core Spec Vol 3 Part H Section 1.3.
-    /// An encrypted SIRK cannot resolve RSIs until decrypted.
-    pub fn resolves_rsi(&self, rsi: &[u8; 6]) -> bool {
-        if self.sirk.sirk_type() == SirkType::Encrypted {
-            return false;
-        }
-
-        // Per CSIP v1.1 Section 4.8, the two most significant bits of prand
-        // (bits 7 and 6 of rsi[5]) must be 0 and 1 (0x40).
-        if (rsi[5] & 0xc0) != 0x40 {
-            return false;
-        }
-
-        let mut prand = [0u8; 3];
-        prand.copy_from_slice(&rsi[3..6]);
-
-        let hash_expected = &rsi[0..3];
-        let hash_computed = set_identity_hash(self.sirk.value(), &prand);
-
-        hash_computed == *hash_expected
-    }
-}
-
-/// Set Identity Hash (SIH) function as defined in CSIS 1.1 Specification,
-/// Section 4.7. `sih(k, r) = e(k, r') mod 2^24`
-/// where `r' = padding || r`, padding is 104 bits of 0.
-/// The LSO of r becomes the LSO of r'.
-fn set_identity_hash(key: &[u8; 16], r: &[u8; 3]) -> [u8; 3] {
-    let mut plaintext = [0u8; 16];
-
-    // r' = padding || r. LSO of r is LSO of r'.
-    // Since Bluetooth is little endian, LSO is at index 0.
-    plaintext[0] = r[0];
-    plaintext[1] = r[1];
-    plaintext[2] = r[2];
-    // The rest of the bytes are already 0 (padding).
-
-    // Bluetooth `e` function uses AES-128 but is LSB-first (little endian).
-    // The `aes` crate is MSB-first. We need to reverse the input and output.
-    let mut key_reversed = *key;
-    key_reversed.reverse();
-    plaintext.reverse();
-
-    let key_block = key_reversed.into();
-    let mut block = plaintext.into();
-
-    let cipher = aes::Aes128::new(&key_block);
-    cipher.encrypt_block(&mut block);
-
-    let mut output = [0u8; 16];
-    output.copy_from_slice(block.as_slice());
-    output.reverse();
-
-    // Output of security function e is truncated to 24 bits by taking the least
-    // significant 24 bits. LSO is at index 0.
-    [output[0], output[1], output[2]]
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
     use bt_common::PeerId;
-    use bt_csis::types::COORDINATED_SET_IDENTIFICATION_SERVICE_UUID;
-    use bt_gatt::central::PeerName;
+    use bt_csis::types::{SirkType, COORDINATED_SET_IDENTIFICATION_SERVICE_UUID};
+    use bt_gatt::central::{AdvertisingDatum, PeerName};
 
-    // Test vector from CSIS Spec v1.1 Appendix A.1
-    // Given k (SIRK): 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22,
-    // 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 (LSO to MSO) Given r
-    // (prand): 0x63, 0xf5, 0x69 (LSO first; in MSB-first notation 0x69f563)
-    // Result hash: 0xda, 0x48, 0x19 (LSO first; in MSB-first notation
-    // 0x1948da)
+    // Test vector from CSIS Spec v1.1 Appendix A.1, least significant octet
+    // first.
     const SAMPLE_KEY: [u8; 16] = [
         0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d,
         0x45,
     ];
-    const SAMPLE_PRAND: [u8; 3] = [0x63, 0xf5, 0x69];
-    const SAMPLE_HASH: [u8; 3] = [0xda, 0x48, 0x19];
 
     // RSI = hash || prand. In LSO first: bytes 0..3 are hash, bytes 3..6 are
     // prand.
     const SAMPLE_RSI: [u8; 6] = [0xda, 0x48, 0x19, 0x63, 0xf5, 0x69];
 
     #[test]
-    fn set_identity_hash_test() {
-        let hash_computed = set_identity_hash(&SAMPLE_KEY, &SAMPLE_PRAND);
-        assert_eq!(hash_computed, SAMPLE_HASH);
-    }
-
-    #[test]
-    fn resolves_rsi() {
-        let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SAMPLE_KEY);
-        let set = CoordinatedSet::new(sirk);
-
-        assert!(set.resolves_rsi(&SAMPLE_RSI));
-
-        // Encrypted SIRK cannot resolve RSI
-        let encrypted_sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, SAMPLE_KEY);
-        let encrypted_set = CoordinatedSet::new(encrypted_sirk);
-        assert!(!encrypted_set.resolves_rsi(&SAMPLE_RSI));
-
-        // Invalid prand MSBs (bit 7 and bit 6 of rsi[5] != 0x40)
-        let mut invalid_rsi = SAMPLE_RSI;
-        invalid_rsi[5] = 0x00; // Bit 6 is 0 instead of 1
-        assert!(!set.resolves_rsi(&invalid_rsi));
-    }
-
-    #[test]
     fn matches_scan_result_rsi() {
         let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SAMPLE_KEY);
         let set = CoordinatedSet::new(sirk);