1use fips203::ml_kem_768;
8use fips203::traits::SerDes as KyberSerDes;
9use fips203::traits::{Decaps, Encaps, KeyGen};
10use fips204::ml_dsa_65;
11use fips204::traits::{SerDes, Signer, Verifier};
12use rand_chacha::ChaCha20Rng;
13use rand_core::SeedableRng;
14use serde::{Deserialize, Serialize};
15use std::fs;
16use std::path::Path;
17
18use crate::constants::{GENERIC_SIGN_CONTEXT, TX_SIGN_CONTEXT};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PQKeypair {
22 pub secret: Vec<u8>,
23 pub public: Vec<u8>,
24}
25
26#[derive(Clone)]
27pub struct DualKeypair {
28 pub dilithium_pk: ml_dsa_65::PublicKey,
29 pub dilithium_sk: ml_dsa_65::PrivateKey,
30 pub kyber_ek: ml_kem_768::EncapsKey,
31 pub kyber_dk: ml_kem_768::DecapsKey,
32 pub mnemonic: String,
33}
34
35impl PQKeypair {
36 pub fn generate() -> Self {
37 let (pk, sk) =
38 ml_dsa_65::try_keygen_with_rng(&mut rand::thread_rng()).expect("Key generation failed");
39
40 Self {
41 secret: sk.into_bytes().to_vec(),
42 public: pk.into_bytes().to_vec(),
43 }
44 }
45
46 pub fn from_seed(seed: &[u8; 32]) -> Self {
47 let mut rng = ChaCha20Rng::from_seed(*seed);
48 let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut rng).expect("Key generation failed");
49
50 Self {
51 secret: sk.into_bytes().to_vec(),
52 public: pk.into_bytes().to_vec(),
53 }
54 }
55
56 pub fn sign(&self, message: &[u8]) -> Vec<u8> {
57 let sk_bytes: [u8; 4032] = self
58 .secret
59 .as_slice()
60 .try_into()
61 .expect("Invalid secret key length");
62 let sk = ml_dsa_65::PrivateKey::try_from_bytes(sk_bytes).expect("Invalid secret key");
63
64 sk.try_sign(message, GENERIC_SIGN_CONTEXT)
65 .expect("Signing failed")
66 .to_vec()
67 }
68
69 pub fn verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> bool {
70 let pk_bytes: [u8; 1952] = match public_key.try_into() {
71 Ok(b) => b,
72 Err(_) => return false,
73 };
74
75 let sig_bytes: [u8; 3309] = match signature.try_into() {
76 Ok(b) => b,
77 Err(_) => return false,
78 };
79
80 if let Ok(pk) = ml_dsa_65::PublicKey::try_from_bytes(pk_bytes) {
81 pk.verify(message, &sig_bytes, GENERIC_SIGN_CONTEXT)
82 } else {
83 false
84 }
85 }
86
87 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
88 let json = serde_json::to_string_pretty(self)
89 .map_err(|e| format!("Failed to serialize: {}", e))?;
90 fs::write(path, json).map_err(|e| format!("Failed to write file: {}", e))?;
91 Ok(())
92 }
93
94 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
95 let json = fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
96 serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize: {}", e))
97 }
98}
99
100impl DualKeypair {
101 pub fn generate() -> Self {
102 let (pk, sk) =
103 ml_dsa_65::try_keygen_with_rng(&mut rand::thread_rng()).expect("Key generation failed");
104 let (ek, dk) = ml_kem_768::KG::try_keygen().expect("Kyber key generation failed");
105
106 Self {
107 dilithium_pk: pk,
108 dilithium_sk: sk,
109 kyber_ek: ek,
110 kyber_dk: dk,
111 mnemonic: String::from("random-generation"),
112 }
113 }
114
115 pub fn from_mnemonic(mnemonic: String) -> Self {
116 Self::from_mnemonic_with_passphrase(mnemonic, "")
117 }
118
119 pub fn from_mnemonic_with_passphrase(mnemonic: String, passphrase: &str) -> Self {
120 let seed = Self::mnemonic_to_seed(&mnemonic, passphrase);
121
122 let dilithium_seed = Self::derive_child_seed(&seed, b"dilithium");
123 let mut rng = ChaCha20Rng::from_seed(dilithium_seed);
124 let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut rng).expect("Key generation failed");
125
126 let kyber_seed = Self::derive_child_seed(&seed, b"kyber");
127 let mut kyber_rng = ChaCha20Rng::from_seed(kyber_seed);
128 let (ek, dk) = ml_kem_768::KG::try_keygen_with_rng(&mut kyber_rng)
129 .expect("Kyber key generation failed");
130
131 Self {
132 dilithium_pk: pk,
133 dilithium_sk: sk,
134 kyber_ek: ek,
135 kyber_dk: dk,
136 mnemonic,
137 }
138 }
139
140 fn mnemonic_to_seed(mnemonic: &str, passphrase: &str) -> [u8; 64] {
141 use pbkdf2::pbkdf2_hmac;
142 use sha2::Sha512;
143
144 let salt = format!("mnemonic{}", passphrase);
145 let mut seed = [0u8; 64];
146 pbkdf2_hmac::<Sha512>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed);
147 seed
148 }
149
150 fn derive_child_seed(master_seed: &[u8], context: &[u8]) -> [u8; 32] {
151 use hkdf::Hkdf;
152 use sha2::Sha256;
153
154 let hk = Hkdf::<Sha256>::new(Some(context), master_seed);
155 let mut okm = [0u8; 32];
156 hk.expand(b"truthlinked-v1", &mut okm)
157 .expect("HKDF expand failed");
158 okm
159 }
160
161 pub fn save_with_password<P: AsRef<Path>>(
162 &self,
163 path: P,
164 password: Option<&str>,
165 ) -> Result<(), String> {
166 let data = serde_json::json!({
167 "mnemonic": self.mnemonic,
168 "dilithium_public": hex::encode(self.dilithium_pk.clone().into_bytes()),
169 "kyber_encaps_key": hex::encode(self.kyber_ek.clone().into_bytes()),
170 "dilithium_secret": hex::encode(self.dilithium_sk.clone().into_bytes()),
171 "kyber_decaps_key": hex::encode(self.kyber_dk.clone().into_bytes()),
172 });
173
174 let json_str = serde_json::to_string_pretty(&data)
175 .map_err(|e| format!("Failed to serialize keypair: {}", e))?;
176
177 let final_data = if let Some(pwd) = password {
178 let encrypted = Self::encrypt_keyfile(&json_str, pwd)?;
179 serde_json::to_string_pretty(&serde_json::json!({
180 "encrypted": true,
181 "version": 1,
182 "data": encrypted,
183 }))
184 .map_err(|e| format!("Failed to wrap encrypted keypair: {}", e))?
185 } else {
186 json_str
187 };
188
189 let path_ref = path.as_ref();
190 fs::write(path_ref, final_data).map_err(|e| e.to_string())?;
191
192 #[cfg(unix)]
193 {
194 use std::os::unix::fs::PermissionsExt;
195 let mut perms = fs::metadata(path_ref)
196 .map_err(|e| e.to_string())?
197 .permissions();
198 perms.set_mode(0o600);
199 fs::set_permissions(path_ref, perms).map_err(|e| e.to_string())?;
200 }
201
202 Ok(())
203 }
204
205 fn encrypt_keyfile(plaintext: &str, password: &str) -> Result<String, String> {
206 use aes_gcm::aead::Aead;
207 use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
208 use rand::RngCore;
209
210 let mut salt = [0u8; 32];
211 rand::thread_rng().fill_bytes(&mut salt);
212
213 let mut key = [0u8; 32];
214 argon2::Argon2::default()
215 .hash_password_into(password.as_bytes(), &salt, &mut key)
216 .map_err(|e| format!("Argon2 failed: {}", e))?;
217
218 let cipher =
219 Aes256Gcm::new_from_slice(&key).map_err(|e| format!("Cipher init failed: {}", e))?;
220
221 let mut nonce_bytes = [0u8; 12];
222 rand::thread_rng().fill_bytes(&mut nonce_bytes);
223 let nonce = Nonce::from_slice(&nonce_bytes);
224
225 let ciphertext = cipher
226 .encrypt(nonce, plaintext.as_bytes())
227 .map_err(|e| format!("Encrypt failed: {}", e))?;
228
229 let payload = serde_json::json!({
230 "salt": hex::encode(salt),
231 "nonce": hex::encode(nonce_bytes),
232 "ciphertext": hex::encode(ciphertext),
233 });
234
235 Ok(payload.to_string())
236 }
237
238 fn decrypt_keyfile(payload: &str, password: &str) -> Result<String, String> {
239 use aes_gcm::aead::Aead;
240 use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
241
242 let obj: serde_json::Value = serde_json::from_str(payload)
243 .map_err(|e| format!("Invalid encrypted keyfile: {}", e))?;
244
245 let salt_hex = obj
246 .get("salt")
247 .and_then(|v| v.as_str())
248 .ok_or("Missing salt")?;
249 let nonce_hex = obj
250 .get("nonce")
251 .and_then(|v| v.as_str())
252 .ok_or("Missing nonce")?;
253 let ct_hex = obj
254 .get("ciphertext")
255 .and_then(|v| v.as_str())
256 .ok_or("Missing ciphertext")?;
257
258 let salt = hex::decode(salt_hex).map_err(|e| format!("Invalid salt: {}", e))?;
259 let nonce = hex::decode(nonce_hex).map_err(|e| format!("Invalid nonce: {}", e))?;
260 let ciphertext = hex::decode(ct_hex).map_err(|e| format!("Invalid ciphertext: {}", e))?;
261
262 let mut key = [0u8; 32];
263 argon2::Argon2::default()
264 .hash_password_into(password.as_bytes(), &salt, &mut key)
265 .map_err(|e| format!("Argon2 failed: {}", e))?;
266
267 let cipher =
268 Aes256Gcm::new_from_slice(&key).map_err(|e| format!("Cipher init failed: {}", e))?;
269
270 let nonce = Nonce::from_slice(&nonce);
271 let plaintext = cipher
272 .decrypt(nonce, ciphertext.as_ref())
273 .map_err(|e| format!("Decrypt failed: {}", e))?;
274
275 String::from_utf8(plaintext).map_err(|e| format!("Invalid UTF-8: {}", e))
276 }
277
278 pub fn load_with_password<P: AsRef<Path>>(
279 path: P,
280 password: Option<&str>,
281 ) -> Result<Self, String> {
282 let path_ref = path.as_ref();
283 let json =
284 fs::read_to_string(path_ref).map_err(|e| format!("Failed to read file: {}", e))?;
285
286 let wrapper: serde_json::Value =
287 serde_json::from_str(&json).map_err(|e| format!("Failed to parse keypair: {}", e))?;
288
289 let is_encrypted = wrapper
290 .get("encrypted")
291 .and_then(|v| v.as_bool())
292 .unwrap_or(false);
293
294 let data_json = if is_encrypted {
295 let pwd = password.ok_or("Password required")?;
296 let payload = wrapper
297 .get("data")
298 .and_then(|v| v.as_str())
299 .ok_or("Missing data")?;
300 Self::decrypt_keyfile(payload, pwd)?
301 } else {
302 json
303 };
304
305 let obj: serde_json::Value = serde_json::from_str(&data_json)
306 .map_err(|e| format!("Failed to parse keypair: {}", e))?;
307
308 let mnemonic = obj
309 .get("mnemonic")
310 .and_then(|v| v.as_str())
311 .unwrap_or("")
312 .to_string();
313 let dilithium_public = obj
314 .get("dilithium_public")
315 .and_then(|v| v.as_str())
316 .ok_or("Missing dilithium_public")?;
317 let dilithium_secret = obj
318 .get("dilithium_secret")
319 .and_then(|v| v.as_str())
320 .ok_or("Missing dilithium_secret")?;
321
322 let pk_bytes: [u8; 1952] = hex::decode(dilithium_public)
323 .map_err(|e| e.to_string())?
324 .try_into()
325 .map_err(|_| "Invalid public key length")?;
326 let sk_bytes: [u8; 4032] = hex::decode(dilithium_secret)
327 .map_err(|e| e.to_string())?
328 .try_into()
329 .map_err(|_| "Invalid secret key length")?;
330
331 let dilithium_pk = ml_dsa_65::PublicKey::try_from_bytes(pk_bytes)
332 .map_err(|e| format!("Invalid public key: {:?}", e))?;
333 let dilithium_sk = ml_dsa_65::PrivateKey::try_from_bytes(sk_bytes)
334 .map_err(|e| format!("Invalid secret key: {:?}", e))?;
335
336 let kyber_encaps_key = obj.get("kyber_encaps_key").and_then(|v| v.as_str());
337 let kyber_decaps_key = obj.get("kyber_decaps_key").and_then(|v| v.as_str());
338
339 let (kyber_ek, kyber_dk) = match (kyber_encaps_key, kyber_decaps_key) {
340 (Some(ek_hex), Some(dk_hex)) => {
341 let ek_bytes: [u8; 1184] = hex::decode(ek_hex)
342 .map_err(|e| e.to_string())?
343 .try_into()
344 .map_err(|_| "Invalid encaps key length")?;
345 let dk_bytes: [u8; 2400] = hex::decode(dk_hex)
346 .map_err(|e| e.to_string())?
347 .try_into()
348 .map_err(|_| "Invalid decaps key length")?;
349 let ek = ml_kem_768::EncapsKey::try_from_bytes(ek_bytes)
350 .map_err(|e| format!("Invalid encaps key: {:?}", e))?;
351 let dk = ml_kem_768::DecapsKey::try_from_bytes(dk_bytes)
352 .map_err(|e| format!("Invalid decaps key: {:?}", e))?;
353 (ek, dk)
354 }
355 _ => {
356 if mnemonic.is_empty() {
357 return Err("Missing kyber keys and mnemonic".to_string());
358 }
359 let derived = Self::from_mnemonic(mnemonic.clone());
360 (derived.kyber_ek, derived.kyber_dk)
361 }
362 };
363
364 Ok(Self {
365 dilithium_pk,
366 dilithium_sk,
367 kyber_ek,
368 kyber_dk,
369 mnemonic,
370 })
371 }
372
373 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
374 let path_ref = path.as_ref();
375 let json =
376 fs::read_to_string(path_ref).map_err(|e| format!("Failed to read file: {}", e))?;
377
378 let wrapper: serde_json::Value =
379 serde_json::from_str(&json).map_err(|e| format!("Failed to parse keypair: {}", e))?;
380
381 let is_encrypted = wrapper
382 .get("encrypted")
383 .and_then(|v| v.as_bool())
384 .unwrap_or(false);
385
386 if is_encrypted {
387 let password = rpassword::prompt_password("Enter password: ")
388 .map_err(|e| format!("Failed to read password: {}", e))?;
389 Self::load_with_password(path_ref, Some(&password))
390 } else {
391 Self::load_with_password(path_ref, None)
392 }
393 }
394
395 pub fn sign_transaction(
396 &self,
397 tx: &crate::pq_execution::Transaction,
398 ) -> Result<crate::pq_execution::Transaction, String> {
399 let mut msg = Vec::new();
400 msg.extend_from_slice(&(tx.genesis_fingerprint.len() as u32).to_le_bytes());
401 msg.extend_from_slice(&tx.genesis_fingerprint);
402 msg.extend_from_slice(&(tx.sender.len() as u32).to_le_bytes());
403 msg.extend_from_slice(&tx.sender);
404 msg.extend_from_slice(&tx.nonce.to_le_bytes());
405 msg.extend_from_slice(&tx.timestamp.to_le_bytes());
406 msg.extend_from_slice(&tx.expiration_height.to_le_bytes());
407
408 let intent_bytes = postcard::to_allocvec(&tx.intent)
409 .map_err(|e| format!("Failed to serialize intent: {}", e))?;
410 msg.extend_from_slice(&(intent_bytes.len() as u32).to_le_bytes());
411 msg.extend_from_slice(&intent_bytes);
412
413 let signature = (&self.dilithium_sk)
414 .try_sign(&msg, TX_SIGN_CONTEXT)
415 .map_err(|e| format!("Signing failed: {:?}", e))?
416 .to_vec();
417
418 Ok(crate::pq_execution::Transaction {
419 sender: tx.sender,
420 intent: tx.intent.clone(),
421 signature,
422 nonce: tx.nonce,
423 timestamp: tx.timestamp,
424 genesis_fingerprint: tx.genesis_fingerprint,
425 expiration_height: tx.expiration_height,
426 })
427 }
428}
429
430pub fn account_id_from_pubkey(pubkey: &[u8]) -> [u8; 32] {
431 use sha2::{Digest, Sha256};
432 let mut hasher = Sha256::new();
433 hasher.update(b"truthlinked-account-id-v1");
434 hasher.update(pubkey);
435 hasher.finalize().into()
436}
437
438fn build_tx_signing_message(
439 genesis_fingerprint: [u8; 32],
440 sender: [u8; 32],
441 timestamp: u64,
442 expiration_height: u64,
443 intent_bytes: &[u8],
444) -> Vec<u8> {
445 let mut msg = Vec::new();
446 msg.extend_from_slice(&(genesis_fingerprint.len() as u32).to_le_bytes());
447 msg.extend_from_slice(&genesis_fingerprint);
448 msg.extend_from_slice(&(sender.len() as u32).to_le_bytes());
449 msg.extend_from_slice(&sender);
450 msg.extend_from_slice(×tamp.to_le_bytes());
451 msg.extend_from_slice(&expiration_height.to_le_bytes());
452 msg.extend_from_slice(&(intent_bytes.len() as u32).to_le_bytes());
453 msg.extend_from_slice(intent_bytes);
454 msg
455}
456
457pub fn sign_transaction(
458 genesis_fingerprint: [u8; 32],
459 sender: [u8; 32],
460 timestamp: u64,
461 expiration_height: u64,
462 intent: &[u8],
463 dilithium_sk: &ml_dsa_65::PrivateKey,
464) -> Vec<u8> {
465 let msg = build_tx_signing_message(genesis_fingerprint, sender, timestamp, expiration_height, intent);
466
467 dilithium_sk
468 .try_sign(&msg, TX_SIGN_CONTEXT)
469 .expect("Signing failed")
470 .to_vec()
471}
472
473pub fn kyber_encapsulate(encaps_key: &ml_kem_768::EncapsKey) -> ([u8; 1088], [u8; 32]) {
474 let (ssk, ct) = encaps_key.try_encaps().expect("Encapsulation failed");
475 (ct.into_bytes(), ssk.into_bytes())
476}
477
478pub fn kyber_decapsulate(decaps_key: &ml_kem_768::DecapsKey, ciphertext: &[u8; 1088]) -> [u8; 32] {
479 let ct = ml_kem_768::CipherText::try_from_bytes(*ciphertext).expect("Invalid ciphertext");
480 decaps_key
481 .try_decaps(&ct)
482 .expect("Decapsulation failed")
483 .into_bytes()
484}