loa_core/
identity.rs

1//! Agent Identity Management
2//!
3//! Handles Ed25519 keypairs for signing and X25519 keypairs for encryption.
4//!
5//! # Key Usage
6//! - **Ed25519**: Currently used for heartbeat authentication (signing messages)
7//! - **X25519**: Reserved for future encrypted agent-to-agent communication
8//!   (encrypted metrics, encrypted commands, etc.). The API stores both public
9//!   keys but only validates Ed25519 signatures currently.
10
11use 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, sync::Arc};
17use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
18
19/// Global agent identity singleton
20///
21/// Initialized once during agent startup and accessible from all actors.
22static AGENT_IDENTITY: OnceCell<Arc<AgentIdentity>> = OnceCell::new();
23
24/// Initialize the global agent identity
25///
26/// This should be called once during agent startup, typically in the builder.
27///
28/// # Panics
29///
30/// Panics if called more than once.
31pub fn init_global_identity(identity: Arc<AgentIdentity>) {
32    AGENT_IDENTITY
33        .set(identity)
34        .expect("Global identity already initialized");
35}
36
37/// Get a clone of the global agent identity
38///
39/// Returns an Arc clone for efficient sharing across actors.
40///
41/// # Panics
42///
43/// Panics if called before `init_global_identity()`.
44pub fn get_global_identity() -> Arc<AgentIdentity> {
45    AGENT_IDENTITY
46        .get()
47        .expect("Global identity not initialized - call init_global_identity() first")
48        .clone()
49}
50
51#[derive(Serialize, Deserialize)]
52struct PersistedKeys {
53    ed25519_bytes: Vec<u8>,
54    x25519_bytes: Vec<u8>,
55}
56
57pub struct AgentIdentity {
58    ed25519_keypair: Keypair,
59    x25519_secret: X25519SecretKey,
60    /// X25519 public key - reserved for future encrypted communication.
61    /// Currently only sent to API during registration; not used locally yet.
62    #[allow(dead_code)]
63    x25519_public: X25519PublicKey,
64    peer_id: PeerId,
65    ed25519_public_key_hex: String,
66    x25519_public_key_hex: String,
67}
68
69// Manual Debug impl that skips secret fields for security
70impl std::fmt::Debug for AgentIdentity {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("AgentIdentity")
73            .field("peer_id", &self.peer_id)
74            .field("ed25519_public_key_hex", &self.ed25519_public_key_hex)
75            .field("x25519_public_key_hex", &self.x25519_public_key_hex)
76            .finish_non_exhaustive()
77    }
78}
79
80impl AgentIdentity {
81    pub fn from_file_or_generate_new(path: &Path) -> Result<Self> {
82        let (ed25519_keypair, x25519_secret, x25519_public) = if path.exists() {
83            Self::load_keypairs_from_file(path)?
84        } else {
85            Self::generate_and_save_new_keypairs(path)?
86        };
87
88        let peer_id = PeerId::from_public_key(&ed25519_keypair.public());
89        let ed25519_public_key_hex = hex::encode(ed25519_keypair.public().encode_protobuf());
90        let x25519_public_key_hex = hex::encode(x25519_public.as_bytes());
91
92        Ok(Self {
93            ed25519_keypair,
94            x25519_secret,
95            x25519_public,
96            peer_id,
97            ed25519_public_key_hex,
98            x25519_public_key_hex,
99        })
100    }
101
102    pub fn peer_id(&self) -> &PeerId {
103        &self.peer_id
104    }
105
106    pub fn ed25519_public_key_hex(&self) -> &str {
107        &self.ed25519_public_key_hex
108    }
109
110    pub fn x25519_public_key_hex(&self) -> &str {
111        &self.x25519_public_key_hex
112    }
113
114    pub fn sign_message_as_hex(&self, message: &[u8]) -> Result<String> {
115        let signature = self
116            .ed25519_keypair
117            .sign(message)
118            .map_err(|e| Self::err("sign", e))?;
119        Ok(hex::encode(signature))
120    }
121
122    /// Decrypt data using x25519 + XChaCha20-Poly1305
123    ///
124    /// **Note:** This method is reserved for future encrypted communication features
125    /// (e.g., encrypted config updates, encrypted metrics). Currently unused but
126    /// maintained for forward compatibility with the encryption package.
127    ///
128    /// Expected format of encrypted_data:
129    /// - First 32 bytes: ephemeral x25519 public key
130    /// - Remaining bytes: XChaCha20-Poly1305 ciphertext (includes auth tag)
131    #[allow(dead_code)]
132    pub fn decrypt_x25519(
133        &self,
134        encrypted_data: &[u8],
135        nonce: &[u8],
136    ) -> std::result::Result<Vec<u8>, String> {
137        use chacha20poly1305::{
138            aead::{Aead, KeyInit},
139            XChaCha20Poly1305, XNonce,
140        };
141
142        // Extract ephemeral public key (first 32 bytes)
143        if encrypted_data.len() < 32 {
144            return Err("encrypted data too short".to_string());
145        }
146
147        let (ephemeral_public_bytes, ciphertext) = encrypted_data.split_at(32);
148        let ephemeral_public_array: [u8; 32] = ephemeral_public_bytes
149            .try_into()
150            .map_err(|_| "invalid ephemeral public key")?;
151        let ephemeral_public = X25519PublicKey::from(ephemeral_public_array);
152
153        // Compute shared secret using Diffie-Hellman
154        let shared_secret = self.x25519_secret.diffie_hellman(&ephemeral_public);
155
156        // Use shared secret as XChaCha20-Poly1305 key (24-byte nonce)
157        let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
158
159        // Decrypt with provided 24-byte nonce
160        let nonce_array: XNonce = nonce
161            .try_into()
162            .map_err(|_| "Invalid nonce length".to_string())?;
163        let plaintext = cipher
164            .decrypt(&nonce_array, ciphertext)
165            .map_err(|e| format!("decryption failed: {}", e))?;
166
167        Ok(plaintext)
168    }
169
170    fn load_keypairs_from_file(path: &Path) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
171        tracing::info!("Loading existing agent identity from {}", path.display());
172
173        // Try loading as JSON (new format)
174        if let Ok(json) = fs::read_to_string(path) {
175            if let Ok(persisted) = serde_json::from_str::<PersistedKeys>(&json) {
176                let mut ed25519_bytes = persisted.ed25519_bytes;
177                let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut ed25519_bytes)
178                    .map_err(|e| Self::err("decode Ed25519 key", e))?;
179
180                let x25519_bytes: [u8; 32] = persisted
181                    .x25519_bytes
182                    .try_into()
183                    .map_err(|_| Self::err("invalid X25519 key length", "expected 32 bytes"))?;
184                let x25519_secret = X25519SecretKey::from(x25519_bytes);
185                let x25519_public = X25519PublicKey::from(&x25519_secret);
186
187                return Ok((ed25519_kp.into(), x25519_secret, x25519_public));
188            }
189        }
190
191        // Fall back to loading old binary format and migrate
192        tracing::info!("Migrating old identity format to JSON");
193        let mut bytes = fs::read(path).map_err(|e| Self::err("read identity file", e))?;
194        let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut bytes)
195            .map_err(|e| Self::err("decode Ed25519 key", e))?;
196
197        // Generate new X25519 keypair for migration
198        let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
199        let x25519_public = X25519PublicKey::from(&x25519_secret);
200
201        // Save in new JSON format
202        let ed25519_full = ed25519_kp.to_bytes();
203        let persisted = PersistedKeys {
204            ed25519_bytes: ed25519_full.to_vec(),
205            x25519_bytes: x25519_secret.to_bytes().to_vec(),
206        };
207        let json = serde_json::to_string_pretty(&persisted)
208            .map_err(|e| Self::err("serialize keys during migration", e))?;
209        fs::write(path, json).map_err(|e| Self::err("write migrated identity file", e))?;
210
211        tracing::info!("Identity migration complete");
212        Ok((ed25519_kp.into(), x25519_secret, x25519_public))
213    }
214
215    fn generate_and_save_new_keypairs(
216        path: &Path,
217    ) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
218        tracing::info!(
219            "Generating new agent identity (saved to {})",
220            path.display()
221        );
222
223        // Generate random bytes for keypair
224        let mut bytes = [0u8; 32];
225        use rand::RngCore;
226        OsRng.fill_bytes(&mut bytes);
227        let ed25519_kp = ed25519::Keypair::from(
228            ed25519::SecretKey::try_from_bytes(&mut bytes)
229                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?,
230        );
231        let ed25519_bytes = ed25519_kp.to_bytes();
232
233        let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
234        let x25519_public = X25519PublicKey::from(&x25519_secret);
235        let x25519_bytes = x25519_secret.to_bytes();
236
237        let persisted = PersistedKeys {
238            ed25519_bytes: ed25519_bytes.to_vec(),
239            x25519_bytes: x25519_bytes.to_vec(),
240        };
241
242        let json =
243            serde_json::to_string_pretty(&persisted).map_err(|e| Self::err("serialize keys", e))?;
244        fs::write(path, json).map_err(|e| Self::err("write identity file", e))?;
245
246        Ok((ed25519_kp.into(), x25519_secret, x25519_public))
247    }
248
249    fn err(context: &str, error: impl std::fmt::Display) -> Error {
250        Error::Io(std::io::Error::new(
251            std::io::ErrorKind::Other,
252            format!("{}: {}", context, error),
253        ))
254    }
255}