1use crate::{Error, Result};
12use libp2p_identity::{ed25519, Keypair, PeerId};
13use once_cell::sync::OnceCell;
14use rand::rngs::OsRng;
15use serde::{Deserialize, Serialize};
16use std::{fs, path::Path, path::PathBuf, sync::Arc};
17use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
18
19#[derive(Debug, Clone)]
24pub struct AgentInfo {
25 pub peer_id: PeerId,
27 pub name: String,
29 pub storage_path: PathBuf,
31 pub dashboard_url: String,
33 pub ed25519_public_key_hex: String,
35 pub x25519_public_key_hex: String,
37 pub claim_token: Option<String>,
39}
40
41impl AgentInfo {
42 pub fn read(storage_path: &Path) -> Result<Self> {
68 let identity_path = storage_path.join("agent_id.key");
69
70 if !identity_path.exists() {
71 return Err(Error::Config(format!(
72 "Agent not initialized. No identity found at {}",
73 identity_path.display()
74 )));
75 }
76
77 let identity = AgentIdentity::from_file_or_generate_new(&identity_path)?;
79
80 let claim_token_path = storage_path.join("claim_token");
82 let claim_token = if claim_token_path.exists() {
83 fs::read_to_string(&claim_token_path)
84 .ok()
85 .map(|s| s.trim().to_string())
86 .filter(|s| !s.is_empty())
87 } else {
88 None
89 };
90
91 let peer_id = identity.peer_id().clone();
92 let peer_id_str = peer_id.to_string();
93
94 let name = format!("agent-{}", &peer_id_str[..8.min(peer_id_str.len())]);
96
97 let api_url = crate::constants::api_url();
99 let dashboard_url = if api_url.contains("localhost") {
100 format!("http://localhost:3000/agents/{}", peer_id)
101 } else if api_url.contains("workers.dev") {
102 format!("https://loa-web.pages.dev/agents/{}", peer_id)
104 } else {
105 format!("https://loa.sh/agents/{}", peer_id)
107 };
108
109 Ok(Self {
110 peer_id,
111 name,
112 storage_path: storage_path.to_path_buf(),
113 dashboard_url,
114 ed25519_public_key_hex: identity.ed25519_public_key_hex().to_string(),
115 x25519_public_key_hex: identity.x25519_public_key_hex().to_string(),
116 claim_token,
117 })
118 }
119
120 pub fn exists(storage_path: &Path) -> bool {
124 storage_path.join("agent_id.key").exists()
125 }
126}
127
128static AGENT_IDENTITY: OnceCell<Arc<AgentIdentity>> = OnceCell::new();
132
133pub fn init_global_identity(identity: Arc<AgentIdentity>) {
141 AGENT_IDENTITY
142 .set(identity)
143 .expect("Global identity already initialized");
144}
145
146pub fn get_global_identity() -> Arc<AgentIdentity> {
154 AGENT_IDENTITY
155 .get()
156 .expect("Global identity not initialized - call init_global_identity() first")
157 .clone()
158}
159
160#[derive(Serialize, Deserialize)]
161struct PersistedKeys {
162 ed25519_bytes: Vec<u8>,
163 x25519_bytes: Vec<u8>,
164}
165
166pub struct AgentIdentity {
167 ed25519_keypair: Keypair,
168 x25519_secret: X25519SecretKey,
169 #[allow(dead_code)]
172 x25519_public: X25519PublicKey,
173 peer_id: PeerId,
174 ed25519_public_key_hex: String,
175 x25519_public_key_hex: String,
176}
177
178impl std::fmt::Debug for AgentIdentity {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.debug_struct("AgentIdentity")
182 .field("peer_id", &self.peer_id)
183 .field("ed25519_public_key_hex", &self.ed25519_public_key_hex)
184 .field("x25519_public_key_hex", &self.x25519_public_key_hex)
185 .finish_non_exhaustive()
186 }
187}
188
189impl AgentIdentity {
190 pub fn from_file_or_generate_new(path: &Path) -> Result<Self> {
191 let (ed25519_keypair, x25519_secret, x25519_public) = if path.exists() {
192 Self::load_keypairs_from_file(path)?
193 } else {
194 Self::generate_and_save_new_keypairs(path)?
195 };
196
197 let peer_id = PeerId::from_public_key(&ed25519_keypair.public());
198 let ed25519_public_key_hex = hex::encode(ed25519_keypair.public().encode_protobuf());
199 let x25519_public_key_hex = hex::encode(x25519_public.as_bytes());
200
201 Ok(Self {
202 ed25519_keypair,
203 x25519_secret,
204 x25519_public,
205 peer_id,
206 ed25519_public_key_hex,
207 x25519_public_key_hex,
208 })
209 }
210
211 pub fn peer_id(&self) -> &PeerId {
212 &self.peer_id
213 }
214
215 pub fn ed25519_public_key_hex(&self) -> &str {
216 &self.ed25519_public_key_hex
217 }
218
219 pub fn x25519_public_key_hex(&self) -> &str {
220 &self.x25519_public_key_hex
221 }
222
223 pub fn sign_message_as_hex(&self, message: &[u8]) -> Result<String> {
224 let signature = self
225 .ed25519_keypair
226 .sign(message)
227 .map_err(|e| Self::err("sign", e))?;
228 Ok(hex::encode(signature))
229 }
230
231 #[allow(dead_code)]
241 pub fn decrypt_x25519(
242 &self,
243 encrypted_data: &[u8],
244 nonce: &[u8],
245 ) -> std::result::Result<Vec<u8>, String> {
246 use chacha20poly1305::{
247 aead::{Aead, KeyInit},
248 XChaCha20Poly1305, XNonce,
249 };
250
251 if encrypted_data.len() < 32 {
253 return Err("encrypted data too short".to_string());
254 }
255
256 let (ephemeral_public_bytes, ciphertext) = encrypted_data.split_at(32);
257 let ephemeral_public_array: [u8; 32] = ephemeral_public_bytes
258 .try_into()
259 .map_err(|_| "invalid ephemeral public key")?;
260 let ephemeral_public = X25519PublicKey::from(ephemeral_public_array);
261
262 let shared_secret = self.x25519_secret.diffie_hellman(&ephemeral_public);
264
265 let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
267
268 let nonce_array: XNonce = nonce
270 .try_into()
271 .map_err(|_| "Invalid nonce length".to_string())?;
272 let plaintext = cipher
273 .decrypt(&nonce_array, ciphertext)
274 .map_err(|e| format!("decryption failed: {}", e))?;
275
276 Ok(plaintext)
277 }
278
279 fn load_keypairs_from_file(path: &Path) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
280 tracing::info!("Loading existing agent identity from {}", path.display());
281
282 if let Ok(json) = fs::read_to_string(path) {
284 if let Ok(persisted) = serde_json::from_str::<PersistedKeys>(&json) {
285 let mut ed25519_bytes = persisted.ed25519_bytes;
286 let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut ed25519_bytes)
287 .map_err(|e| Self::err("decode Ed25519 key", e))?;
288
289 let x25519_bytes: [u8; 32] = persisted
290 .x25519_bytes
291 .try_into()
292 .map_err(|_| Self::err("invalid X25519 key length", "expected 32 bytes"))?;
293 let x25519_secret = X25519SecretKey::from(x25519_bytes);
294 let x25519_public = X25519PublicKey::from(&x25519_secret);
295
296 return Ok((ed25519_kp.into(), x25519_secret, x25519_public));
297 }
298 }
299
300 tracing::info!("Migrating old identity format to JSON");
302 let mut bytes = fs::read(path).map_err(|e| Self::err("read identity file", e))?;
303 let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut bytes)
304 .map_err(|e| Self::err("decode Ed25519 key", e))?;
305
306 let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
308 let x25519_public = X25519PublicKey::from(&x25519_secret);
309
310 let ed25519_full = ed25519_kp.to_bytes();
312 let persisted = PersistedKeys {
313 ed25519_bytes: ed25519_full.to_vec(),
314 x25519_bytes: x25519_secret.to_bytes().to_vec(),
315 };
316 let json = serde_json::to_string_pretty(&persisted)
317 .map_err(|e| Self::err("serialize keys during migration", e))?;
318 fs::write(path, json).map_err(|e| Self::err("write migrated identity file", e))?;
319
320 tracing::info!("Identity migration complete");
321 Ok((ed25519_kp.into(), x25519_secret, x25519_public))
322 }
323
324 fn generate_and_save_new_keypairs(
325 path: &Path,
326 ) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
327 tracing::info!(
328 "Generating new agent identity (saved to {})",
329 path.display()
330 );
331
332 let mut bytes = [0u8; 32];
334 use rand::RngCore;
335 OsRng.fill_bytes(&mut bytes);
336 let ed25519_kp = ed25519::Keypair::from(
337 ed25519::SecretKey::try_from_bytes(&mut bytes)
338 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?,
339 );
340 let ed25519_bytes = ed25519_kp.to_bytes();
341
342 let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
343 let x25519_public = X25519PublicKey::from(&x25519_secret);
344 let x25519_bytes = x25519_secret.to_bytes();
345
346 let persisted = PersistedKeys {
347 ed25519_bytes: ed25519_bytes.to_vec(),
348 x25519_bytes: x25519_bytes.to_vec(),
349 };
350
351 let json =
352 serde_json::to_string_pretty(&persisted).map_err(|e| Self::err("serialize keys", e))?;
353 fs::write(path, json).map_err(|e| Self::err("write identity file", e))?;
354
355 Ok((ed25519_kp.into(), x25519_secret, x25519_public))
356 }
357
358 fn err(context: &str, error: impl std::fmt::Display) -> Error {
359 Error::Io(std::io::Error::new(
360 std::io::ErrorKind::Other,
361 format!("{}: {}", context, error),
362 ))
363 }
364}