1use crate::crypto::{CryptoProvider, DefaultProvider, verify_identity};
2use crate::{
3 DdError, ErrorCode, HashAlgorithm, PeerId, PublicIdentity, Result, TrustState, hex_decode,
4 hex_encode,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9pub use crate::crypto::PrivateIdentity;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ContactCard {
13 pub name: Option<String>,
14 pub identity: String,
15 pub fingerprint: String,
16 pub capabilities: Vec<String>,
17 pub public: PublicIdentity,
18}
19
20impl ContactCard {
21 pub fn from_public(name: Option<String>, public: PublicIdentity) -> Result<Self> {
22 let peer = verify_identity(&public)?;
23 let fp = fingerprint(&public);
24 Ok(Self {
25 name,
26 identity: peer.to_string(),
27 fingerprint: fp,
28 capabilities: Vec::new(),
29 public,
30 })
31 }
32
33 pub fn to_text(&self) -> String {
34 format!(
35 "DD Contact\nName: {}\nIdentity: {}\nFingerprint: {}\nCapabilities: {}\n",
36 self.name.as_deref().unwrap_or(""),
37 self.identity,
38 self.fingerprint,
39 self.capabilities.join(",")
40 )
41 }
42
43 pub fn to_ddcontact(&self) -> Result<String> {
44 serde_json::to_string_pretty(self).map_err(|e| DdError::crypto(e.to_string()))
45 }
46
47 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
48 if bytes.starts_with(b"DD Contact") {
49 return parse_text_card(std::str::from_utf8(bytes).map_err(|_| {
50 DdError::protocol(ErrorCode::Ddp1001InvalidFrame, "contact not utf-8")
51 })?);
52 }
53 serde_json::from_slice(bytes).map_err(|e| DdError::invalid_frame(e.to_string()))
54 }
55
56 pub fn peer_id(&self) -> Result<PeerId> {
57 verify_identity(&self.public)
58 }
59}
60
61pub fn fingerprint(public: &PublicIdentity) -> String {
62 let p = DefaultProvider;
63 let mut buf = Vec::from(&b"ddp-fp-v2"[..]);
64 buf.extend_from_slice(&public.ed25519_pk);
65 buf.extend_from_slice(&public.x25519_pk);
66 let d = p.hash(HashAlgorithm::Blake3, &buf);
67 hex_encode(&d.0[..8])
68}
69
70pub fn display_fingerprint(public: &PublicIdentity) -> String {
72 let h = fingerprint(public).to_ascii_uppercase();
73 if h.len() >= 8 {
74 format!("DD-{}-{}", &h[..4], &h[4..8])
75 } else {
76 format!("DD-{h}")
77 }
78}
79
80fn parse_text_card(s: &str) -> Result<ContactCard> {
81 let mut name = None;
82 let mut identity = String::new();
83 let mut fingerprint = String::new();
84 let mut capabilities = Vec::new();
85 for line in s.lines() {
86 if let Some(v) = line.strip_prefix("Name: ") {
87 name = Some(v.trim().to_string());
88 } else if let Some(v) = line.strip_prefix("Identity: ") {
89 identity = v.trim().to_string();
90 } else if let Some(v) = line.strip_prefix("Fingerprint: ") {
91 fingerprint = v.trim().to_string();
92 } else if let Some(v) = line.strip_prefix("Capabilities: ") {
93 capabilities = v
94 .split(',')
95 .filter(|x| !x.is_empty())
96 .map(|x| x.trim().to_string())
97 .collect();
98 }
99 }
100 Err(DdError::protocol(
101 ErrorCode::Ddp1001InvalidFrame,
102 format!(
103 "text contact {identity} {fingerprint} {name:?} {capabilities:?} requires JSON public keys"
104 ),
105 ))
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Contact {
110 pub card: ContactCard,
111 pub trust: TrustState,
112}
113
114#[derive(Debug, Default)]
115pub struct ContactBook {
116 inner: HashMap<PeerId, Contact>,
117}
118
119impl ContactBook {
120 pub fn insert(&mut self, mut contact: Contact) -> Result<PeerId> {
121 let id = contact.card.peer_id()?;
122 if contact.trust == TrustState::Unknown {
123 contact.trust = TrustState::Known;
124 }
125 self.inner.insert(id, contact);
126 Ok(id)
127 }
128
129 pub fn observe(&mut self, public: PublicIdentity) -> Result<PeerId> {
130 let id = verify_identity(&public)?;
131 self.inner.entry(id).or_insert_with(|| Contact {
132 card: ContactCard::from_public(None, public).expect("verified"),
133 trust: TrustState::Observed,
134 });
135 Ok(id)
136 }
137
138 pub fn get(&self, id: &PeerId) -> Option<&Contact> {
139 self.inner.get(id)
140 }
141
142 pub fn get_mut(&mut self, id: &PeerId) -> Option<&mut Contact> {
143 self.inner.get_mut(id)
144 }
145
146 pub fn resolve(&self, spec: &str) -> Result<(PeerId, PublicIdentity)> {
147 let raw = spec.trim();
148 if raw.is_empty() {
149 return Err(DdError::protocol(
150 ErrorCode::Ddi5001UnknownContact,
151 "empty peer",
152 ));
153 }
154 let named: Vec<_> = self
155 .inner
156 .iter()
157 .filter(|(_, c)| {
158 c.trust != TrustState::Blocked
159 && c.card
160 .name
161 .as_deref()
162 .is_some_and(|n| n.eq_ignore_ascii_case(raw))
163 })
164 .collect();
165 match named.len() {
166 1 => {
167 let (id, c) = named[0];
168 return Ok((*id, c.card.public.clone()));
169 }
170 n if n > 1 => {
171 return Err(DdError::protocol(
172 ErrorCode::Ddp1005BadIdentifier,
173 "ambiguous contact name",
174 ));
175 }
176 _ => {}
177 }
178 let rest = raw.strip_prefix("dd:").unwrap_or(raw).to_ascii_lowercase();
179 if rest.len() < 8 {
180 return Err(DdError::protocol(
181 ErrorCode::Ddi5001UnknownContact,
182 format!(
183 "unknown name '{raw}' (import a .ddcontact, or use dd: plus at least 8 hex chars)"
184 ),
185 ));
186 }
187 let matches: Vec<_> = self
188 .inner
189 .iter()
190 .filter(|(id, c)| {
191 c.trust != TrustState::Blocked && hex_encode(id.as_bytes()).starts_with(&rest)
192 })
193 .collect();
194 match matches.len() {
195 1 => {
196 let (id, c) = matches[0];
197 Ok((*id, c.card.public.clone()))
198 }
199 0 => Err(DdError::protocol(
200 ErrorCode::Ddi5001UnknownContact,
201 "no matching contact",
202 )),
203 _ => Err(DdError::protocol(
204 ErrorCode::Ddp1005BadIdentifier,
205 "ambiguous prefix",
206 )),
207 }
208 }
209
210 pub fn set_trust(&mut self, id: &PeerId, trust: TrustState) {
211 if let Some(c) = self.inner.get_mut(id) {
212 c.trust = trust;
213 }
214 }
215
216 pub fn all(&self) -> impl Iterator<Item = (&PeerId, &Contact)> {
217 self.inner.iter()
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct IdentityTransition {
224 pub old_peer: PeerId,
225 pub new_public: PublicIdentity,
226 pub created_at: u64,
227 #[serde(with = "serde_bytes")]
228 pub signature: [u8; 64],
229}
230
231impl IdentityTransition {
232 pub fn issue(old: &PrivateIdentity, new_public: PublicIdentity, now: u64) -> Result<Self> {
233 let new_id = verify_identity(&new_public)?;
234 let mut msg = Vec::from(&b"ddp-rotate-v2"[..]);
235 msg.extend_from_slice(old.peer_id.as_bytes());
236 msg.extend_from_slice(new_id.as_bytes());
237 msg.extend_from_slice(&now.to_be_bytes());
238 let sig = DefaultProvider.sign(&old.signing_key(), &msg);
239 Ok(Self {
240 old_peer: old.peer_id,
241 new_public,
242 created_at: now,
243 signature: sig,
244 })
245 }
246
247 pub fn verify(&self, old_public: &PublicIdentity) -> Result<PeerId> {
248 let old_id = verify_identity(old_public)?;
249 if old_id != self.old_peer {
250 return Err(DdError::crypto("transition old peer mismatch"));
251 }
252 let new_id = verify_identity(&self.new_public)?;
253 let mut msg = Vec::from(&b"ddp-rotate-v2"[..]);
254 msg.extend_from_slice(old_id.as_bytes());
255 msg.extend_from_slice(new_id.as_bytes());
256 msg.extend_from_slice(&self.created_at.to_be_bytes());
257 DefaultProvider.verify(&old_public.ed25519_pk, &msg, &self.signature)?;
258 Ok(new_id)
259 }
260}
261
262#[derive(Serialize, Deserialize)]
263pub struct IdentityFile {
264 pub version: u8,
265 pub ed25519_secret_hex: String,
266 pub x25519_secret_hex: String,
267}
268
269impl IdentityFile {
270 pub fn from_private(id: &PrivateIdentity) -> Self {
271 Self {
272 version: 2,
273 ed25519_secret_hex: hex_encode(&id.ed25519_bytes()),
274 x25519_secret_hex: hex_encode(&id.x25519_bytes()),
275 }
276 }
277
278 pub fn into_private(self) -> Result<PrivateIdentity> {
279 let ed = decode32(&self.ed25519_secret_hex)?;
280 let x = decode32(&self.x25519_secret_hex)?;
281 Ok(PrivateIdentity::from_secrets(ed, x))
282 }
283}
284
285fn decode32(h: &str) -> Result<[u8; 32]> {
286 let v = hex_decode(h)?;
287 if v.len() != 32 {
288 return Err(DdError::crypto("expected 32 bytes"));
289 }
290 let mut a = [0u8; 32];
291 a.copy_from_slice(&v);
292 Ok(a)
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn rotate_attests() {
301 let old = PrivateIdentity::generate();
302 let new = PrivateIdentity::generate();
303 let t = IdentityTransition::issue(&old, new.public.clone(), 10).unwrap();
304 assert_eq!(t.verify(&old.public).unwrap(), new.peer_id);
305 }
306
307 #[test]
308 fn resolve_short_name() {
309 let a = PrivateIdentity::generate();
310 let mut book = ContactBook::default();
311 book.insert(Contact {
312 card: ContactCard::from_public(Some("laptop".into()), a.public.clone()).unwrap(),
313 trust: TrustState::Known,
314 })
315 .unwrap();
316 let (id, _) = book.resolve("laptop").unwrap();
317 assert_eq!(id, a.peer_id);
318 assert!(book.resolve("nope").is_err());
319 }
320}