1use ed25519_dalek::{Signer, SigningKey};
2use libp2p::identity::{self, Keypair};
3use libp2p::PeerId;
4use sha2::{Digest, Sha256};
5
6use crate::error::{HuddleError, Result};
7
8pub struct Identity {
9 signing_key: SigningKey,
10 libp2p_keypair: Keypair,
11 peer_id: PeerId,
12 fingerprint: String,
13}
14
15impl Identity {
16 pub fn generate() -> Result<Self> {
17 let mut rng = rand::thread_rng();
18 let signing_key = SigningKey::generate(&mut rng);
19 Self::from_signing_key(signing_key)
20 }
21
22 pub fn from_secret_bytes(bytes: [u8; 32]) -> Result<Self> {
23 let signing_key = SigningKey::from_bytes(&bytes);
24 Self::from_signing_key(signing_key)
25 }
26
27 fn from_signing_key(signing_key: SigningKey) -> Result<Self> {
28 let secret = signing_key.to_bytes();
29 let public = signing_key.verifying_key().to_bytes();
30 let mut combined = [0u8; 64];
31 combined[..32].copy_from_slice(&secret);
32 combined[32..].copy_from_slice(&public);
33
34 let ed25519_kp = identity::ed25519::Keypair::try_from_bytes(&mut combined)
35 .map_err(|e| HuddleError::Identity(e.to_string()))?;
36 let libp2p_keypair = Keypair::from(ed25519_kp);
37 let peer_id = PeerId::from(libp2p_keypair.public());
38 let fingerprint = compute_fingerprint(&public);
39
40 Ok(Self {
41 signing_key,
42 libp2p_keypair,
43 peer_id,
44 fingerprint,
45 })
46 }
47
48 pub fn fingerprint(&self) -> &str {
49 &self.fingerprint
50 }
51
52 pub fn peer_id(&self) -> PeerId {
53 self.peer_id
54 }
55
56 pub fn keypair(&self) -> &Keypair {
57 &self.libp2p_keypair
58 }
59
60 pub fn secret_bytes(&self) -> [u8; 32] {
61 self.signing_key.to_bytes()
62 }
63
64 pub fn public_bytes(&self) -> [u8; 32] {
65 self.signing_key.verifying_key().to_bytes()
66 }
67
68 pub fn sign(&self, msg: &[u8]) -> [u8; 64] {
73 self.signing_key.sign(msg).to_bytes()
74 }
75}
76
77pub fn compute_fingerprint(public_key: &[u8; 32]) -> String {
83 let hash = Sha256::digest(public_key);
84 let hex_str = hex::encode(&hash[..12]);
85 hex_str
86 .as_bytes()
87 .chunks(4)
88 .map(|chunk| std::str::from_utf8(chunk).unwrap())
89 .collect::<Vec<&str>>()
90 .join("-")
91}
92
93pub const RELAY_AUTH_DOMAIN: &[u8] = b"huddle-relay-auth-v1";
100
101pub fn relay_auth_msg(nonce: &[u8]) -> Vec<u8> {
106 let mut m = Vec::with_capacity(RELAY_AUTH_DOMAIN.len() + nonce.len());
107 m.extend_from_slice(RELAY_AUTH_DOMAIN);
108 m.extend_from_slice(nonce);
109 m
110}
111
112pub fn safety_code(public_key: &[u8; 32]) -> String {
120 let hash = Sha256::digest(public_key);
121 let hex_str = hex::encode(&hash[..6]).to_ascii_uppercase();
122 let groups: Vec<&str> = hex_str
123 .as_bytes()
124 .chunks(4)
125 .map(|chunk| std::str::from_utf8(chunk).unwrap())
126 .collect();
127 format!("SAFE-{}", groups.join("-"))
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn fingerprint_is_deterministic() {
136 let key_bytes = [42u8; 32];
137 let id = Identity::from_secret_bytes(key_bytes).unwrap();
138 let id2 = Identity::from_secret_bytes(key_bytes).unwrap();
139 assert_eq!(id.fingerprint(), id2.fingerprint());
140 }
141
142 #[test]
143 fn fingerprint_format_is_correct() {
144 let id = Identity::generate().unwrap();
145 let fp = id.fingerprint();
146 let parts: Vec<&str> = fp.split('-').collect();
147 assert_eq!(parts.len(), 6);
148 for part in &parts {
149 assert_eq!(part.len(), 4);
150 assert!(part.chars().all(|c| c.is_ascii_hexdigit()));
151 }
152 }
153
154 #[test]
155 fn different_keys_produce_different_fingerprints() {
156 let id1 = Identity::generate().unwrap();
157 let id2 = Identity::generate().unwrap();
158 assert_ne!(id1.fingerprint(), id2.fingerprint());
159 }
160
161 #[test]
162 fn round_trip_through_bytes() {
163 let id1 = Identity::generate().unwrap();
164 let bytes = id1.secret_bytes();
165 let id2 = Identity::from_secret_bytes(bytes).unwrap();
166 assert_eq!(id1.fingerprint(), id2.fingerprint());
167 assert_eq!(id1.peer_id(), id2.peer_id());
168 }
169
170 #[test]
171 fn peer_id_is_derived_from_same_key() {
172 let id = Identity::generate().unwrap();
173 let pid = id.peer_id();
174 assert!(!pid.to_string().is_empty());
175 }
176
177 #[test]
178 fn safety_code_is_stable_and_well_formed() {
179 let key = [7u8; 32];
180 let a = safety_code(&key);
181 let b = safety_code(&key);
182 assert_eq!(a, b);
183 assert!(a.starts_with("SAFE-"));
184 let groups: Vec<&str> = a.trim_start_matches("SAFE-").split('-').collect();
185 assert_eq!(groups.len(), 3);
186 for g in &groups {
187 assert_eq!(g.len(), 4);
188 assert!(g.chars().all(|c| c.is_ascii_hexdigit() && c.is_ascii_uppercase() || c.is_ascii_digit()));
189 }
190 }
191}