1use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
20use base64::Engine as _;
21use serde::{Deserialize, Serialize};
22use std::fmt;
23use std::str::FromStr;
24use thiserror::Error;
25
26#[derive(Debug, Error)]
28pub enum DidError {
29 #[error("invalid DID format: {0}")]
30 InvalidFormat(String),
31 #[error("unsupported DID method: {0}")]
32 UnsupportedMethod(String),
33 #[error("invalid UBL type: {0}")]
34 InvalidType(String),
35 #[error("invalid key encoding: {0}")]
36 InvalidKeyEncoding(String),
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
41#[serde(rename_all = "lowercase")]
42pub enum DidMethod {
43 #[default]
45 Ubl,
46 Key,
48 Web,
50}
51
52impl fmt::Display for DidMethod {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 DidMethod::Ubl => write!(f, "ubl"),
56 DidMethod::Key => write!(f, "key"),
57 DidMethod::Web => write!(f, "web"),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "lowercase")]
65pub enum DidType {
66 User,
68 Org,
70 Agent,
72 App,
74 Wallet,
76 Service,
78}
79
80impl fmt::Display for DidType {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 DidType::User => write!(f, "user"),
84 DidType::Org => write!(f, "org"),
85 DidType::Agent => write!(f, "agent"),
86 DidType::App => write!(f, "app"),
87 DidType::Wallet => write!(f, "wallet"),
88 DidType::Service => write!(f, "service"),
89 }
90 }
91}
92
93impl FromStr for DidType {
94 type Err = DidError;
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 match s {
97 "user" => Ok(DidType::User),
98 "org" => Ok(DidType::Org),
99 "agent" => Ok(DidType::Agent),
100 "app" => Ok(DidType::App),
101 "wallet" => Ok(DidType::Wallet),
102 "service" => Ok(DidType::Service),
103 _ => Err(DidError::InvalidType(s.to_string())),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub struct Did {
111 #[serde(rename = "id")]
113 raw: String,
114 #[serde(skip)]
116 method: DidMethod,
117}
118
119impl Did {
120 pub fn parse(s: &str) -> Result<Self, DidError> {
122 if !s.starts_with("did:") {
123 return Err(DidError::InvalidFormat("must start with 'did:'".into()));
124 }
125 let parts: Vec<&str> = s.splitn(3, ':').collect();
126 if parts.len() < 3 {
127 return Err(DidError::InvalidFormat("missing method or id".into()));
128 }
129 let method = match parts[1] {
130 "ubl" => DidMethod::Ubl,
131 "key" => DidMethod::Key,
132 "web" => DidMethod::Web,
133 m => return Err(DidError::UnsupportedMethod(m.to_string())),
134 };
135 Ok(Self {
136 raw: s.to_string(),
137 method,
138 })
139 }
140
141 pub fn ubl(typ: DidType, id: &str) -> Self {
143 Self {
144 raw: format!("did:ubl:{}:{}", typ, id),
145 method: DidMethod::Ubl,
146 }
147 }
148
149 pub fn key_from_ed25519(pk32: &[u8; 32]) -> Self {
151 let raw = format!("did:key:z{}", B64URL.encode(pk32));
152 Self {
153 raw,
154 method: DidMethod::Key,
155 }
156 }
157
158 pub fn key_from_ed25519_w3c(pk32: &[u8; 32]) -> Self {
160 let mut v = vec![0xed, 0x01]; v.extend_from_slice(pk32);
162 let raw = format!("did:key:z{}", bs58::encode(v).into_string());
163 Self {
164 raw,
165 method: DidMethod::Key,
166 }
167 }
168
169 pub fn web(domain: &str) -> Self {
171 Self {
172 raw: format!("did:web:{}", domain),
173 method: DidMethod::Web,
174 }
175 }
176
177 pub fn as_str(&self) -> &str {
179 &self.raw
180 }
181
182 pub fn method(&self) -> &DidMethod {
184 &self.method
185 }
186
187 pub fn ubl_type(&self) -> Option<DidType> {
189 if self.method != DidMethod::Ubl {
190 return None;
191 }
192 let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
193 if parts.len() >= 3 {
194 parts[2].parse().ok()
195 } else {
196 None
197 }
198 }
199
200 pub fn ubl_id(&self) -> Option<&str> {
202 if self.method != DidMethod::Ubl {
203 return None;
204 }
205 let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
206 if parts.len() >= 4 {
207 Some(parts[3])
208 } else {
209 None
210 }
211 }
212
213 pub fn key_bytes(&self) -> Option<[u8; 32]> {
215 if self.method != DidMethod::Key {
216 return None;
217 }
218 let key_part = self.raw.strip_prefix("did:key:z")?;
219 let bytes = B64URL.decode(key_part.as_bytes()).ok()?;
220 bytes.try_into().ok()
221 }
222
223 pub fn can_sign(&self) -> bool {
225 matches!(self.method, DidMethod::Key | DidMethod::Ubl)
226 }
227}
228
229impl fmt::Display for Did {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(f, "{}", self.raw)
232 }
233}
234
235impl FromStr for Did {
236 type Err = DidError;
237 fn from_str(s: &str) -> Result<Self, Self::Err> {
238 Did::parse(s)
239 }
240}
241
242impl AsRef<str> for Did {
243 fn as_ref(&self) -> &str {
244 &self.raw
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_did_ubl() {
254 let did = Did::ubl(DidType::User, "daniel");
255 assert_eq!(did.as_str(), "did:ubl:user:daniel");
256 assert_eq!(did.method(), &DidMethod::Ubl);
257 assert_eq!(did.ubl_type(), Some(DidType::User));
258 assert_eq!(did.ubl_id(), Some("daniel"));
259 }
260
261 #[test]
262 fn test_did_key() {
263 let pk = [1u8; 32];
264 let did = Did::key_from_ed25519(&pk);
265 assert!(did.as_str().starts_with("did:key:z"));
266 assert_eq!(did.method(), &DidMethod::Key);
267 assert_eq!(did.key_bytes(), Some(pk));
268 }
269
270 #[test]
271 fn test_did_parse() {
272 let did = Did::parse("did:ubl:agent:gpt4").unwrap();
273 assert_eq!(did.ubl_type(), Some(DidType::Agent));
274 assert_eq!(did.ubl_id(), Some("gpt4"));
275 }
276}