1//! Immutable, opaque identifiers.2//!3//! Handles such as `@alice` are presentation names only. Every authorization decision in4//! the system is made against these IDs, which never change and are never reassigned.56use std::fmt;7use std::str::FromStr;89use serde::{Deserialize, Deserializer, Serialize, Serializer};1011const ENCODING: data_encoding::Encoding = data_encoding::BASE32_NOPAD;1213macro_rules! opaque_id {14 ($name:ident, $prefix:literal) => {15 16 pub struct $name([u8; 16]);1718 impl $name {19 pub const PREFIX: &'static str = $prefix;2021 pub const fn from_bytes(bytes: [u8; 16]) -> Self {22 Self(bytes)23 }2425 /// Mint a fresh, unguessable id.26 pub fn generate() -> Self {27 let mut bytes = [0u8; 16];28 getrandom::fill(&mut bytes).expect("system randomness is available");29 Self(bytes)30 }3132 pub fn as_bytes(&self) -> &[u8; 16] {33 &self.034 }35 }3637 impl fmt::Display for $name {38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {39 write!(f, "{}_{}", $prefix, ENCODING.encode(&self.0).to_lowercase())40 }41 }4243 impl fmt::Debug for $name {44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {45 write!(f, "{}", self)46 }47 }4849 impl FromStr for $name {50 type Err = IdParseError;5152 fn from_str(s: &str) -> Result<Self, Self::Err> {53 let body = s54 .strip_prefix(concat!($prefix, "_"))55 .ok_or(IdParseError::BadPrefix { expected: $prefix })?;56 let raw = ENCODING57 .decode(body.to_uppercase().as_bytes())58 .map_err(|_| IdParseError::BadEncoding)?;59 let bytes: [u8; 16] = raw.try_into().map_err(|_| IdParseError::BadLength)?;60 Ok(Self(bytes))61 }62 }6364 // Strings in JSON (readable API payloads), raw bytes in postcard (compact wire65 // frames and stable signing input).66 impl Serialize for $name {67 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {68 if s.is_human_readable() {69 s.serialize_str(&self.to_string())70 } else {71 self.0.serialize(s)72 }73 }74 }7576 impl<'de> Deserialize<'de> for $name {77 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {78 if d.is_human_readable() {79 let text = String::deserialize(d)?;80 text.parse().map_err(serde::de::Error::custom)81 } else {82 <[u8; 16]>::deserialize(d).map(Self)83 }84 }85 }86 };87}888990pub enum IdParseError {91 92 BadPrefix { expected: &'static str },93 94 BadEncoding,95 96 BadLength,97}9899opaque_id!(AccountId, "acct");100opaque_id!(ResourceId, "res");101102103mod tests {104 use super::*;105106 107 fn round_trips_through_string() {108 let id = ResourceId::from_bytes([7u8; 16]);109 let text = id.to_string();110 assert!(text.starts_with("res_"));111 assert_eq!(ResourceId::from_str(&text).unwrap(), id);112 }113114 115 fn rejects_the_wrong_prefix() {116 let account = AccountId::from_bytes([3u8; 16]).to_string();117 // An AccountId must never parse as a ResourceId, or a confused-deputy bug becomes118 // possible where one namespace's id authorizes access in the other.119 assert_eq!(120 ResourceId::from_str(&account),121 Err(IdParseError::BadPrefix { expected: "res" })122 );123 }124125 126 fn rejects_truncated_ids() {127 assert_eq!(128 ResourceId::from_str("res_aaaa"),129 Err(IdParseError::BadLength)130 );131 }132133 134 fn postcard_round_trip_is_compact() {135 let id = ResourceId::from_bytes([9u8; 16]);136 let bytes = postcard::to_allocvec(&id).unwrap();137 assert_eq!(138 bytes.len(),139 16,140 "ids must not be length-prefixed on the wire"141 );142 assert_eq!(postcard::from_bytes::<ResourceId>(&bytes).unwrap(), id);143 }144}