saorsa_gossip_identity/
lib.rs1#![warn(missing_docs)]
2
3use anyhow::{Context, Result};
8use saorsa_gossip_types::PeerId;
9use serde::{Deserialize, Serialize};
10use std::path::Path;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct MlDsaKeyPair {
18 pub public_key: Vec<u8>,
20 secret_key: Vec<u8>,
22}
23
24impl MlDsaKeyPair {
25 pub fn generate() -> Result<Self> {
29 use saorsa_pqc::{MlDsa65, MlDsaOperations};
30
31 let signer = MlDsa65::new();
32 let (pk, sk) = signer.generate_keypair()?;
33
34 Ok(Self {
35 public_key: pk.as_bytes().to_vec(),
36 secret_key: sk.as_bytes().to_vec(),
37 })
38 }
39
40 pub fn from_keypair_bytes(public_key: Vec<u8>, secret_key: Vec<u8>) -> Self {
53 Self {
54 public_key,
55 secret_key,
56 }
57 }
58
59 pub fn public_key(&self) -> &[u8] {
61 &self.public_key
62 }
63
64 pub fn secret_key(&self) -> &[u8] {
66 &self.secret_key
67 }
68
69 pub fn get_secret_key_typed(&self) -> Result<saorsa_pqc::MlDsaSecretKey> {
71 Ok(saorsa_pqc::MlDsaSecretKey::from_bytes(&self.secret_key)?)
72 }
73
74 pub fn peer_id(&self) -> PeerId {
76 PeerId::from_pubkey(&self.public_key)
77 }
78
79 pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
87 use saorsa_pqc::{MlDsa65, MlDsaOperations};
88
89 let signer = MlDsa65::new();
90 let sk = self.get_secret_key_typed()?;
91 let signature = signer.sign(&sk, message)?;
92 Ok(signature.as_bytes().to_vec())
93 }
94
95 pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<bool> {
105 use saorsa_pqc::{MlDsa65, MlDsaOperations, MlDsaPublicKey, MlDsaSignature};
106
107 let verifier = MlDsa65::new();
108 let pk = MlDsaPublicKey::from_bytes(public_key)?;
109 let sig = MlDsaSignature::from_bytes(signature)?;
110
111 Ok(verifier.verify(&pk, message, &sig)?)
112 }
113
114 pub fn to_bytes(&self) -> Result<Vec<u8>> {
116 postcard::to_stdvec(self).context("Failed to serialize keypair")
117 }
118
119 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
121 postcard::from_bytes(bytes).context("Failed to deserialize keypair")
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Identity {
128 key_pair: MlDsaKeyPair,
130 alias: String,
132}
133
134impl Identity {
135 pub fn new(alias: String) -> Result<Self> {
137 Ok(Self {
138 key_pair: MlDsaKeyPair::generate()?,
139 alias,
140 })
141 }
142
143 pub async fn load_or_create(
154 four_words: &str,
155 display_name: &str,
156 keystore_path: &str,
157 ) -> Result<Self> {
158 match Self::load_from_keystore(four_words, keystore_path).await {
160 Ok(identity) => Ok(identity),
161 Err(_) => {
162 let identity = Self::new(display_name.to_string())?;
164
165 identity.save_to_keystore(four_words, keystore_path).await?;
167
168 Ok(identity)
169 }
170 }
171 }
172
173 pub async fn load_from_keystore(four_words: &str, keystore_path: &str) -> Result<Self> {
179 let file_path = Self::keystore_file_path(four_words, keystore_path);
180
181 let data = tokio::fs::read(&file_path).await.context(format!(
183 "Failed to read keystore file: {}",
184 file_path.display()
185 ))?;
186
187 let identity: Identity =
189 postcard::from_bytes(&data).context("Failed to deserialize identity")?;
190
191 Ok(identity)
192 }
193
194 pub async fn save_to_keystore(&self, four_words: &str, keystore_path: &str) -> Result<()> {
200 let file_path = Self::keystore_file_path(four_words, keystore_path);
201
202 if let Some(parent) = file_path.parent() {
204 tokio::fs::create_dir_all(parent)
205 .await
206 .context("Failed to create keystore directory")?;
207 }
208
209 let data = postcard::to_stdvec(&self).context("Failed to serialize identity")?;
211
212 tokio::fs::write(&file_path, data).await.context(format!(
214 "Failed to write keystore file: {}",
215 file_path.display()
216 ))?;
217
218 Ok(())
219 }
220
221 fn keystore_file_path(four_words: &str, keystore_path: &str) -> std::path::PathBuf {
223 let safe_filename = four_words.replace('-', "_");
224 Path::new(keystore_path).join(format!("{}.identity", safe_filename))
225 }
226
227 pub fn alias(&self) -> &str {
229 &self.alias
230 }
231
232 pub fn peer_id(&self) -> PeerId {
234 self.key_pair.peer_id()
235 }
236
237 pub fn key_pair(&self) -> &MlDsaKeyPair {
239 &self.key_pair
240 }
241}
242
243#[cfg(test)]
244#[allow(clippy::unwrap_used, clippy::expect_used)]
245mod tests {
246 use super::*;
247 use tempfile::TempDir;
248
249 #[test]
250 fn test_keypair_generation() {
251 let keypair = MlDsaKeyPair::generate();
252 assert!(keypair.is_ok());
253 }
254
255 #[test]
256 fn test_identity_creation() {
257 let identity = Identity::new("Alice".to_string());
258 assert!(identity.is_ok());
259
260 if let Ok(id) = identity {
261 assert_eq!(id.alias(), "Alice");
262 }
263 }
264
265 #[test]
266 fn test_peer_id_derivation() {
267 let keypair = MlDsaKeyPair::generate().ok();
268 if let Some(kp) = keypair {
269 let peer_id = kp.peer_id();
270 assert_eq!(peer_id.as_bytes().len(), 32);
271 }
272 }
273
274 #[tokio::test]
277 async fn test_load_or_create_new_identity() {
278 let temp_dir = TempDir::new().expect("temp dir");
280 let keystore_path = temp_dir.path().to_str().expect("path");
281
282 let four_words = "ocean-forest-moon-star";
283 let display_name = "Alice";
284
285 let identity = Identity::load_or_create(four_words, display_name, keystore_path)
286 .await
287 .expect("should create new identity");
288
289 assert_eq!(identity.alias(), display_name);
290
291 let peer_id = identity.peer_id();
293 assert_eq!(peer_id.as_bytes().len(), 32);
294 }
295
296 #[tokio::test]
297 async fn test_load_or_create_existing_identity() {
298 let temp_dir = TempDir::new().expect("temp dir");
300 let keystore_path = temp_dir.path().to_str().expect("path");
301
302 let four_words = "ocean-forest-moon-star";
303 let display_name = "Alice";
304
305 let identity1 = Identity::load_or_create(four_words, display_name, keystore_path)
307 .await
308 .expect("should create");
309
310 let peer_id1 = identity1.peer_id();
311
312 let identity2 = Identity::load_or_create(four_words, display_name, keystore_path)
314 .await
315 .expect("should load existing");
316
317 let peer_id2 = identity2.peer_id();
318
319 assert_eq!(peer_id1, peer_id2);
321 assert_eq!(identity2.alias(), display_name);
322 }
323
324 #[tokio::test]
325 async fn test_keystore_persistence() {
326 let temp_dir = TempDir::new().expect("temp dir");
328 let keystore_path = temp_dir.path().to_str().expect("path");
329
330 let four_words = "river-mountain-cloud-light";
331 let identity = Identity::new("Bob".to_string()).expect("create");
332
333 identity
335 .save_to_keystore(four_words, keystore_path)
336 .await
337 .expect("should save");
338
339 let loaded = Identity::load_from_keystore(four_words, keystore_path)
341 .await
342 .expect("should load");
343
344 assert_eq!(identity.peer_id(), loaded.peer_id());
346 assert_eq!(identity.alias(), loaded.alias());
347 }
348
349 #[tokio::test]
350 async fn test_load_nonexistent_identity_fails() {
351 let temp_dir = TempDir::new().expect("temp dir");
353 let keystore_path = temp_dir.path().to_str().expect("path");
354
355 let result = Identity::load_from_keystore("nonexistent-four-words", keystore_path).await;
356
357 assert!(result.is_err());
359 }
360
361 #[tokio::test]
362 async fn test_multiple_identities_in_same_keystore() {
363 let temp_dir = TempDir::new().expect("temp dir");
365 let keystore_path = temp_dir.path().to_str().expect("path");
366
367 let alice = Identity::load_or_create("ocean-forest-moon-star", "Alice", keystore_path)
369 .await
370 .expect("alice");
371
372 let bob = Identity::load_or_create("river-mountain-cloud-light", "Bob", keystore_path)
373 .await
374 .expect("bob");
375
376 assert_ne!(alice.alias(), bob.alias());
378
379 let alice2 = Identity::load_or_create("ocean-forest-moon-star", "Alice", keystore_path)
385 .await
386 .expect("alice2");
387
388 let bob2 = Identity::load_or_create("river-mountain-cloud-light", "Bob", keystore_path)
389 .await
390 .expect("bob2");
391
392 assert_eq!(alice.peer_id(), alice2.peer_id());
394 assert_eq!(alice.alias(), alice2.alias());
395 assert_eq!(bob.peer_id(), bob2.peer_id());
396 assert_eq!(bob.alias(), bob2.alias());
397 }
398}