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}