Skip to main content

deaddrop_core/identity/
mod.rs

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
70/// Human-comparable form `DD-7F91-22BA`. The cryptographic identity remains `dd:`.
71pub 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/// Old identity attests a replacement. Reachability is not silently destroyed.
222#[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}