cotton_unique/
lib.rs

1//! Implementing statistically-unique per-device IDs based on chip IDs
2//!
3//! The cotton-unique crate encapsulates the creation of per-device unique
4//! identifiers -- for things such as Ethernet MAC addresses, or UPnP UUIDs.
5//!
6//! Most microcontrollers (e.g. STM32, RA6M5) have a unique per-unit
7//! identifier built-in; RP2040 does not, but on that platform it's intended
8//! to use the unique identifier in the associated SPI flash chip instead.
9//!
10//! But it's not a good idea to just use the raw chip ID as the MAC
11//! address, for several reasons: it's the wrong size, it's quite
12//! predictable (it's not 96 random bits per chip, it typically
13//! encodes the chip batch number and die position on the wafer, so
14//! two different STM32s might have IDs that differ only in one or two
15//! bits, meaning we can't just pick any 46 bits from the 96 in case
16//! we accidentally pick seldom-changing ones) — and, worst of all, if
17//! anyone were to use the same ID for anything else later, they might
18//! be surprised if it were very closely correlated with the device's
19//! MAC address.
20//!
21//! So the thing to do, is to hash the unique ID along with a key, or
22//! salt, which indicates what we're using it for. The result is thus
23//! deterministic and consistent on any one device for a particular
24//! salt, but varies from one device to another (and from one salt to
25//! another).
26//!
27//! For instance, the cotton-ssdp device tests obtain a MAC address by
28//! hashing the STM32 unique ID with the salt string "stm32-eth", and
29//! UPnP UUIDs by hashing the *same* ID with a *different* salt.
30//!
31//! This does not *guarantee* uniqueness, but if the hash function is
32//! doing its job, the odds of a collision involve a factor of 2^-64 --
33//! or in other words are highly unlikely.
34#![no_std]
35#![warn(missing_docs)]
36#![warn(rustdoc::missing_crate_level_docs)]
37use core::hash::Hasher;
38
39/// An object from which unique identifers can be obtained
40pub struct UniqueId {
41    id: [u64; 2],
42}
43
44impl UniqueId {
45    /// Create a new UniqueId object
46    ///
47    /// The `unique_bytes` can be a raw unique chip ID, as they are hashed
48    /// and salted before any client code sees them.
49    pub fn new(unique_bytes: &[u8; 16]) -> Self {
50        Self {
51            id: [
52                u64::from_le_bytes(unique_bytes[0..8].try_into().unwrap()),
53                u64::from_le_bytes(unique_bytes[8..16].try_into().unwrap()),
54            ],
55        }
56    }
57
58    /// Return a (statistically) unique identifier for a specific purpose
59    ///
60    /// The `salt` string should concisely express the purpose for which the
61    /// identifier is needed; i.e., identifiers for different purposes must
62    /// have different salts.
63    pub fn id(&self, salt: &[u8]) -> u64 {
64        let mut h =
65            siphasher::sip::SipHasher::new_with_keys(self.id[0], self.id[1]);
66        h.write(salt);
67        h.finish()
68    }
69
70    /// Return a (statistically) unique identifier for a specific purpose
71    ///
72    /// This is very similar to `id` but takes two `salt` values, a string
73    /// and a u32. This is intended to be helpful when creating identifiers
74    /// larger than u64; see the implementation of `uuid()` for an example.
75    pub fn id2(&self, salt: &[u8], salt2: u32) -> u64 {
76        let mut h =
77            siphasher::sip::SipHasher::new_with_keys(self.id[0], self.id[1]);
78        h.write(salt);
79        h.write_u32(salt2);
80        h.finish()
81    }
82}
83
84/// Return a statistically-unique but consistent MAC address
85///
86/// The recommendation is that the `salt` string encodes the network
87/// address somehow (so that multi-homed hosts get different MAC
88/// addresses on different interfaces); for instance b"stm32-eth" or
89/// b"w5500-spi0".
90pub fn mac_address(unique: &UniqueId, salt: &[u8]) -> [u8; 6] {
91    let mut mac_address = [0u8; 6];
92    let r = unique.id(salt).to_ne_bytes();
93    mac_address.copy_from_slice(&r[0..6]);
94    mac_address[0] &= 0xFE; // clear multicast bit
95    mac_address[0] |= 2; // set local bit
96    mac_address
97}
98
99/// Return a statistically-unique but consistent UUID
100///
101/// The recommendation is that the `salt` string encodes the purpose of
102/// the UUID somehow.
103pub fn uuid(unique: &UniqueId, salt: &[u8]) -> uuid::Uuid {
104    let mut bytes = [0u8; 16];
105    let u1 = unique.id2(salt, 0).to_be_bytes();
106    bytes[0..8].copy_from_slice(&u1);
107    let u2 = unique.id2(salt, 1).to_be_bytes();
108    bytes[8..16].copy_from_slice(&u2);
109    uuid::Uuid::new_v8(bytes)
110}
111
112#[cfg(feature = "stm32")]
113/// Obtaining a UniqueId on STM32 platforms
114pub mod stm32 {
115    /// Construct a UniqueId for STM32 from the chip unique ID
116    ///
117    /// Also known as "device signature", see for example RM0385 rev5 s41.1.
118    /// All STM32s have a unique ID, but different generations of STM32 have
119    /// it at different fixed locations. The `stm32_device_signature` crate
120    /// can be used to abstract away these differences and return the raw
121    /// 12-byte identifier.
122    pub fn unique_chip_id(id: &'static [u8; 12]) -> super::UniqueId {
123        let mut unique_bytes = [0u8; 16];
124        unique_bytes[0..12].copy_from_slice(id);
125        super::UniqueId::new(&unique_bytes)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    extern crate alloc;
133
134    #[test]
135    fn test_id() {
136        let raw_id = [0u8; 16];
137        let unique = UniqueId::new(&raw_id);
138        let id = unique.id(b"test-vector");
139        // There's nothing particularly magic about this value, the point is
140        // (a) it's not zero, and (b) it never changes from run to run.
141        assert_eq!(11391256791731596036u64, id);
142    }
143
144    #[test]
145    fn test_id2() {
146        let raw_id = [0u8; 16];
147        let unique = UniqueId::new(&raw_id);
148        let id = unique.id2(b"test-vector", 37);
149        assert_eq!(17344812425781864766u64, id);
150    }
151
152    #[test]
153    fn test_saltiness() {
154        let raw_id = [0u8; 16];
155        let unique = UniqueId::new(&raw_id);
156        let id1 = unique.id(b"eth0");
157        let id2 = unique.id(b"eth1");
158        assert_ne!(id1, id2);
159    }
160
161    #[test]
162    fn test_saltiness2() {
163        let raw_id = [0u8; 16];
164        let unique = UniqueId::new(&raw_id);
165        let id1 = unique.id2(b"need-longer-id", 0);
166        let id2 = unique.id2(b"need-longer-id", 1);
167        assert_ne!(id1, id2);
168    }
169
170    #[test]
171    fn test_mac() {
172        let raw_id = [0u8; 16];
173        let unique = UniqueId::new(&raw_id);
174        let mac = mac_address(&unique, b"eth0");
175        assert_eq!(0x62, mac[0]);
176        assert_eq!(0x67, mac[1]);
177        assert_eq!(0x0B, mac[2]);
178        assert_eq!(0xE3, mac[3]);
179        assert_eq!(0xD9, mac[4]);
180        assert_eq!(0xBD, mac[5]);
181    }
182
183    #[test]
184    fn test_uuid() {
185        let raw_id = [0u8; 16];
186        let unique = UniqueId::new(&raw_id);
187        let uuid =
188            alloc::format!("{}", uuid(&unique, b"upnp-media-renderer:0"));
189        assert_eq!("2505b7b1-dfa3-8c2d-8f02-9e3409457472", uuid);
190    }
191}