astrid_crypto/
identifier.rs1use std::fmt;
9
10use crate::{CryptoResult, PublicKey};
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash)]
17pub struct IdentifierHash<T = [u8; 32]>(T);
18
19impl<T> IdentifierHash<T> {
20 #[must_use]
22 pub const fn as_inner(&self) -> &T {
23 &self.0
24 }
25
26 #[must_use]
28 pub fn into_inner(self) -> T {
29 self.0
30 }
31
32 #[must_use]
38 pub fn map<U>(self, map: impl FnOnce(T) -> U) -> IdentifierHash<U> {
39 IdentifierHash(map(self.0))
40 }
41}
42
43impl IdentifierHash<[u8; 32]> {
44 #[must_use]
50 pub fn derive(context: &'static str, value: &[u8]) -> Self {
51 Self(blake3::derive_key(context, value))
52 }
53
54 #[must_use]
56 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
57 Self(bytes)
58 }
59
60 #[must_use]
62 pub const fn as_bytes(&self) -> &[u8; 32] {
63 &self.0
64 }
65
66 #[must_use]
68 pub fn to_hex(&self) -> String {
69 hex::encode(self.0)
70 }
71
72 #[must_use]
74 pub fn to_prefixed_hex(&self) -> String {
75 format!("blake3:{}", self.to_hex())
76 }
77}
78
79impl fmt::Debug for IdentifierHash<[u8; 32]> {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "IdentifierHash({})", self.to_prefixed_hex())
82 }
83}
84
85impl fmt::Display for IdentifierHash<[u8; 32]> {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.write_str(&self.to_prefixed_hex())
88 }
89}
90
91impl AsRef<[u8]> for IdentifierHash<[u8; 32]> {
92 fn as_ref(&self) -> &[u8] {
93 &self.0
94 }
95}
96
97#[derive(Clone, PartialEq, Eq, Hash)]
103pub struct PublicKeyFingerprint<T = String>(T);
104
105impl<T> PublicKeyFingerprint<T> {
106 #[must_use]
108 pub const fn as_inner(&self) -> &T {
109 &self.0
110 }
111
112 #[must_use]
114 pub fn into_inner(self) -> T {
115 self.0
116 }
117
118 #[must_use]
123 pub fn map<U>(self, map: impl FnOnce(T) -> U) -> PublicKeyFingerprint<U> {
124 PublicKeyFingerprint(map(self.0))
125 }
126}
127
128impl PublicKeyFingerprint<String> {
129 #[must_use]
131 pub fn from_public_key(public_key: &PublicKey) -> Self {
132 const CONTEXT: &str = "astrid.runtime.ed25519-public-key.fingerprint.v1";
133 Self(IdentifierHash::derive(CONTEXT, public_key.as_bytes()).to_prefixed_hex())
134 }
135
136 pub fn from_ed25519_hex(value: &str) -> CryptoResult<Self> {
142 let hex = value.strip_prefix("ed25519:").unwrap_or(value);
143 let public_key = PublicKey::from_hex(hex)?;
144 Ok(Self::from_public_key(&public_key))
145 }
146
147 #[must_use]
149 pub fn as_str(&self) -> &str {
150 &self.0
151 }
152}
153
154impl fmt::Debug for PublicKeyFingerprint<String> {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "PublicKeyFingerprint({})", self.0)
157 }
158}
159
160impl fmt::Display for PublicKeyFingerprint<String> {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.write_str(&self.0)
163 }
164}
165
166impl AsRef<str> for PublicKeyFingerprint<String> {
167 fn as_ref(&self) -> &str {
168 self.as_str()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn domains_produce_distinct_identifiers() {
178 let value = b"same input";
179 let invite = IdentifierHash::derive("Astrid invite token 2026-07-15 v1", value);
180 let pair = IdentifierHash::derive("Astrid pair token 2026-07-15 v1", value);
181 assert_ne!(invite, pair);
182 assert_eq!(invite.to_hex().len(), 64);
183 let encoded = invite.map(hex::encode);
184 assert_eq!(encoded.as_inner().len(), 64);
185 }
186
187 #[test]
188 fn public_key_fingerprint_normalizes_supported_hex_forms() {
189 let key = "ab".repeat(32);
190 let bare = PublicKeyFingerprint::from_ed25519_hex(&key).unwrap();
191 let prefixed = PublicKeyFingerprint::from_ed25519_hex(&format!("ed25519:{key}")).unwrap();
192 assert_eq!(bare, prefixed);
193 assert_eq!(
194 bare.as_str(),
195 "blake3:bfae897f0bf656d68f50c4fde6fc273a7d6c8a28feb8c127f7d2cf9bb54d18b8"
196 );
197 assert_eq!(bare.as_str().len(), 71);
198 let bytes = bare.map(String::into_bytes);
199 assert_eq!(bytes.as_inner().len(), 71);
200 }
201
202 #[test]
203 fn public_key_fingerprint_rejects_invalid_keys() {
204 assert!(PublicKeyFingerprint::from_ed25519_hex("abcd").is_err());
205 assert!(PublicKeyFingerprint::from_ed25519_hex(&"zz".repeat(32)).is_err());
206 }
207}