1#![deny(missing_docs)]
36#![cfg_attr(not(test), no_std)]
37
38use core::fmt;
39use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
40use sha2::{Digest as _, Sha256};
41
42pub const SALT_LEN: usize = 32;
44
45pub const MAC_LEN: usize = 6;
47
48pub const SSID_DIGEST_LEN: usize = 8;
50
51pub const REDACTED_MARKER: &str = "*";
56
57const MAC_DOMAIN_TAG: &[u8] = b"fuchsia-diagnostics-redact-mac-v1";
59
60const SSID_DOMAIN_TAG: &[u8] = b"fuchsia-diagnostics-redact-ssid-v1";
62
63#[repr(usize)]
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70enum State {
71 Uninitialized = 0,
73 Initializing = 1,
75 Masking = 2,
77 Salted = 3,
79}
80
81impl TryFrom<usize> for State {
82 type Error = usize;
83
84 fn try_from(value: usize) -> Result<Self, usize> {
85 match value {
86 0 => Ok(State::Uninitialized),
87 1 => Ok(State::Initializing),
88 2 => Ok(State::Masking),
89 3 => Ok(State::Salted),
90 other => Err(other),
91 }
92 }
93}
94
95static REDACTOR_STATE: AtomicUsize = AtomicUsize::new(State::Uninitialized as usize);
99
100fn redactor_state() -> State {
104 State::try_from(REDACTOR_STATE.load(Ordering::Acquire)).expect("invalid redactor state")
105}
106
107fn set_redactor_state(state: State) {
111 REDACTOR_STATE.store(state as usize, Ordering::Release);
112}
113
114fn maybe_change_redactor_state(expected: State, target: State) -> bool {
117 REDACTOR_STATE
118 .compare_exchange(expected as usize, target as usize, Ordering::AcqRel, Ordering::Acquire)
119 .is_ok()
120}
121static GLOBAL_SALT: [AtomicU8; SALT_LEN] = [const { AtomicU8::new(0) }; SALT_LEN];
128
129pub trait Redactor: Send + Sync {
134 fn redact_mac(&self, octets: [u8; MAC_LEN]) -> [u8; MAC_LEN];
139
140 fn redact_ssid(&self, ssid: &[u8]) -> [u8; SSID_DIGEST_LEN];
142}
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
148pub struct MaskingRedactor;
149
150impl Redactor for MaskingRedactor {
151 fn redact_mac(&self, octets: [u8; MAC_LEN]) -> [u8; MAC_LEN] {
152 [octets[0], octets[1], octets[2], 0, 0, 0]
153 }
154
155 fn redact_ssid(&self, _ssid: &[u8]) -> [u8; SSID_DIGEST_LEN] {
156 [0; SSID_DIGEST_LEN]
157 }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub struct SaltedRedactor {
167 salt: [u8; SALT_LEN],
168}
169
170impl SaltedRedactor {
171 pub const fn new(salt: [u8; SALT_LEN]) -> Self {
173 Self { salt }
174 }
175
176 pub const fn salt(&self) -> &[u8; SALT_LEN] {
178 &self.salt
179 }
180}
181
182impl Redactor for SaltedRedactor {
183 fn redact_mac(&self, octets: [u8; MAC_LEN]) -> [u8; MAC_LEN] {
184 let mut hasher = Sha256::new();
185 hasher.update(MAC_DOMAIN_TAG);
186 hasher.update(self.salt);
187 hasher.update(octets);
188 let digest = hasher.finalize();
189 [octets[0], octets[1], octets[2], digest[0], digest[1], digest[2]]
190 }
191
192 fn redact_ssid(&self, ssid: &[u8]) -> [u8; SSID_DIGEST_LEN] {
193 let mut hasher = Sha256::new();
194 hasher.update(SSID_DOMAIN_TAG);
195 hasher.update(self.salt);
196 hasher.update(ssid);
197 let digest = hasher.finalize();
198 let mut out = [0; SSID_DIGEST_LEN];
199 out.copy_from_slice(&digest[..SSID_DIGEST_LEN]);
200 out
201 }
202}
203
204pub trait Redactable {
213 fn fmt_unredacted(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
218
219 fn fmt_redacted(&self, redactor: &dyn Redactor, f: &mut fmt::Formatter<'_>) -> fmt::Result;
226
227 fn fmt_dispatch(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match redactor_state() {
231 State::Uninitialized => self.fmt_unredacted(f),
232 State::Masking | State::Initializing => self.fmt_redacted(&MaskingRedactor, f),
233 State::Salted => self.fmt_redacted(&SaltedRedactor::new(load_salt()), f),
234 }
235 }
236}
237
238pub struct Unredacted<'a, T: ?Sized>(pub &'a T);
245
246impl<T: Redactable + ?Sized> fmt::Display for Unredacted<'_, T> {
247 #[inline]
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 self.0.fmt_unredacted(f)
250 }
251}
252
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub struct SetRedactorError;
257
258impl fmt::Display for SetRedactorError {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 write!(f, "a global redactor has already been installed")
261 }
262}
263
264impl core::error::Error for SetRedactorError {}
265
266pub fn set_masking_redactor() {
271 let _ = maybe_change_redactor_state(State::Uninitialized, State::Masking);
272}
273
274pub fn set_salted_redactor(salt: [u8; SALT_LEN]) -> Result<(), SetRedactorError> {
284 loop {
285 let current = redactor_state();
286 if current != State::Uninitialized && current != State::Masking {
287 return Err(SetRedactorError);
288 }
289 if maybe_change_redactor_state(current, State::Initializing) {
290 for (dst, src) in GLOBAL_SALT.iter().zip(salt) {
294 dst.store(src, Ordering::Relaxed);
295 }
296 set_redactor_state(State::Salted);
297 return Ok(());
298 }
299 }
300}
301
302fn load_salt() -> [u8; SALT_LEN] {
306 core::array::from_fn(|i| GLOBAL_SALT[i].load(Ordering::Relaxed))
307}
308
309#[macro_export]
315macro_rules! impl_display_via_redactable {
316 ($ty:ty) => {
317 impl ::core::fmt::Display for $ty {
318 #[inline]
319 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
320 $crate::Redactable::fmt_dispatch(self, f)
321 }
322 }
323 };
324}
325
326#[macro_export]
330macro_rules! impl_debug_via_redactable {
331 ($ty:ty) => {
332 impl ::core::fmt::Debug for $ty {
333 #[inline]
334 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
335 $crate::Redactable::fmt_dispatch(self, f)
336 }
337 }
338 };
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 const TEST_SALT_1: [u8; SALT_LEN] = [0x11; SALT_LEN];
346 const TEST_SALT_2: [u8; SALT_LEN] = [0x22; SALT_LEN];
347 const TEST_MAC_1: [u8; MAC_LEN] = [0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e];
348 const TEST_MAC_2: [u8; MAC_LEN] = [0x00, 0x1a, 0x2b, 0xff, 0xee, 0xdd];
349
350 struct TestMac([u8; MAC_LEN]);
351
352 fn fmt_mac(m: [u8; MAC_LEN], f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 write!(f, "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5])
354 }
355
356 impl Redactable for TestMac {
357 fn fmt_unredacted(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 fmt_mac(self.0, f)
359 }
360
361 fn fmt_redacted(&self, r: &dyn Redactor, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 fmt_mac(r.redact_mac(self.0), f)?;
363 f.write_str(REDACTED_MARKER)
364 }
365 }
366
367 crate::impl_display_via_redactable!(TestMac);
368 crate::impl_debug_via_redactable!(TestMac);
369
370 struct TestSsid<'a>(&'a [u8]);
371
372 fn fmt_ssid(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 write!(f, "<ssid-")?;
374 for byte in bytes {
375 write!(f, "{byte:02x}")?;
376 }
377 write!(f, ">")
378 }
379
380 impl Redactable for TestSsid<'_> {
381 fn fmt_unredacted(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 fmt_ssid(self.0, f)
383 }
384
385 fn fmt_redacted(&self, r: &dyn Redactor, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 fmt_ssid(&r.redact_ssid(self.0), f)?;
387 f.write_str(REDACTED_MARKER)
388 }
389 }
390
391 crate::impl_display_via_redactable!(TestSsid<'_>);
392
393 fn format_with_redactor<T: Redactable>(item: &T, r: &dyn Redactor) -> String {
394 struct Adapter<'a, T>(&'a T, &'a dyn Redactor);
395 impl<T: Redactable> fmt::Display for Adapter<'_, T> {
396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397 self.0.fmt_redacted(self.1, f)
398 }
399 }
400 format!("{}", Adapter(item, r))
401 }
402
403 fn format_unredacted<T: Redactable>(item: &T) -> String {
404 struct Adapter<'a, T>(&'a T);
405 impl<T: Redactable> fmt::Display for Adapter<'_, T> {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 self.0.fmt_unredacted(f)
408 }
409 }
410 format!("{}", Adapter(item))
411 }
412
413 #[test]
414 fn masking_redactor_preserves_oui_and_zeroes_suffix_and_ssid() {
415 assert_eq!(MaskingRedactor.redact_mac(TEST_MAC_1), [0x00, 0x1a, 0x2b, 0, 0, 0]);
416 assert_eq!(MaskingRedactor.redact_ssid(b"foo"), [0; SSID_DIGEST_LEN]);
417 assert_eq!(
418 format_with_redactor(&TestMac(TEST_MAC_1), &MaskingRedactor),
419 "00:1a:2b:00:00:00*"
420 );
421 assert_eq!(
422 format_with_redactor(&TestSsid(b"foo"), &MaskingRedactor),
423 "<ssid-0000000000000000>*"
424 );
425 }
426
427 #[test]
428 fn salted_redactor_preserves_oui_and_hashes_nic() {
429 let r1 = SaltedRedactor::new(TEST_SALT_1);
430 let r2 = SaltedRedactor::new(TEST_SALT_2);
431 assert_eq!(r1.salt(), &TEST_SALT_1);
432
433 let hashed_1 = format_with_redactor(&TestMac(TEST_MAC_1), &r1);
434 assert!(hashed_1.starts_with("00:1a:2b:"));
435 assert!(hashed_1.ends_with(REDACTED_MARKER));
436 assert_eq!(hashed_1.len(), "00:1a:2b:3c:4d:5e*".len());
437 assert_ne!(hashed_1, "00:1a:2b:3c:4d:5e*");
438
439 assert_eq!(hashed_1, format_with_redactor(&TestMac(TEST_MAC_1), &r1));
441
442 let hashed_2 = format_with_redactor(&TestMac(TEST_MAC_2), &r1);
444 assert!(hashed_2.starts_with("00:1a:2b:"));
445 assert_ne!(hashed_1, hashed_2);
446
447 let hashed_diff_salt = format_with_redactor(&TestMac(TEST_MAC_1), &r2);
449 assert!(hashed_diff_salt.starts_with("00:1a:2b:"));
450 assert_ne!(hashed_1, hashed_diff_salt);
451 }
452
453 #[test]
454 fn salted_redactor_hashes_ssid_deterministically() {
455 let r1 = SaltedRedactor::new(TEST_SALT_1);
456 let r2 = SaltedRedactor::new(TEST_SALT_2);
457
458 let ssid_1 = TestSsid(b"CorpWiFi");
459 let ssid_2 = TestSsid(b"GuestWiFi");
460
461 let hashed_1 = format_with_redactor(&ssid_1, &r1);
462 assert!(hashed_1.starts_with("<ssid-"));
463 assert!(hashed_1.ends_with(">*"));
464 assert_ne!(hashed_1, format_unredacted(&ssid_1));
465 assert_eq!(hashed_1, format_with_redactor(&ssid_1, &r1));
466 assert_ne!(hashed_1, format_with_redactor(&ssid_2, &r1));
467 assert_ne!(hashed_1, format_with_redactor(&ssid_1, &r2));
468 }
469
470 #[test]
474 fn global_state_machine_transitions() {
475 set_redactor_state(State::Uninitialized);
477 let mac = TestMac(TEST_MAC_1);
478 let ssid = TestSsid(b"CorpWiFi");
479
480 assert_eq!(redactor_state(), State::Uninitialized);
482 assert_eq!(format!("{mac}"), "00:1a:2b:3c:4d:5e");
483 assert_eq!(format!("{mac:?}"), "00:1a:2b:3c:4d:5e");
484 assert_eq!(format!("{ssid}"), "<ssid-436f727057694669>");
485
486 set_masking_redactor();
488 assert_eq!(redactor_state(), State::Masking);
489 assert_eq!(format!("{mac}"), "00:1a:2b:00:00:00*");
490 assert_eq!(format!("{ssid}"), "<ssid-0000000000000000>*");
491
492 assert_eq!(set_salted_redactor(TEST_SALT_1), Ok(()));
494 let expected_mac = format_with_redactor(&mac, &SaltedRedactor::new(TEST_SALT_1));
495 let expected_ssid = format_with_redactor(&ssid, &SaltedRedactor::new(TEST_SALT_1));
496 assert_eq!(format!("{mac}"), expected_mac);
497 assert_eq!(format!("{ssid}"), expected_ssid);
498
499 assert_eq!(set_salted_redactor(TEST_SALT_2), Err(SetRedactorError));
501 set_masking_redactor();
502 assert_eq!(format!("{mac}"), expected_mac);
503
504 set_redactor_state(State::Uninitialized);
506 assert_eq!(format!("{mac}"), "00:1a:2b:3c:4d:5e");
507 assert_eq!(set_salted_redactor(TEST_SALT_2), Ok(()));
508 assert_eq!(format!("{mac}"), format_with_redactor(&mac, &SaltedRedactor::new(TEST_SALT_2)));
509 }
510}