1use crate::tools::keys_post_quantum::pq_commitment_bytes_from_seed;
24use crate::tools::tools;
25use crate::tools::types::{PQCommitmentBytes, SignatureKey, VerificationKey, VerificationKeyBytes};
26use anyhow::anyhow;
27use argon2::password_hash::rand_core::OsRng;
28use argon2::password_hash::SaltString;
29use argon2::{Argon2, PasswordHasher};
30use chacha20poly1305::aead::Aead;
31use chacha20poly1305::{AeadCore, ChaCha20Poly1305, Key, KeyInit};
32use std::fmt::{self, Display, Formatter};
33
34#[derive(Clone)]
35pub struct Keys {
36 pub signature_key: SignatureKey,
37 pub verification_key: VerificationKey,
38 pub verification_key_bytes: VerificationKeyBytes,
39 pub pq_commitment_bytes: PQCommitmentBytes,
40}
41
42impl Display for Keys {
43 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44 write!(f, "[ verification_key_bytes:{} pq_commitment:{} ]", hex::encode(self.verification_key.as_ref()), hex::encode(self.pq_commitment_bytes.as_ref()))
45 }
46}
47
48impl Keys {
49 pub fn from_rnd(skip_pq_commitment_bytes: bool) -> anyhow::Result<Keys> {
50 let mut seed = [0u8; 32];
51 tools::random_fill_bytes(&mut seed);
52 Self::from_seed(&seed, skip_pq_commitment_bytes)
53 }
54
55 pub fn from_phrase(phrase: &str) -> anyhow::Result<Keys> {
56 let mut seed = [0u8; 32];
57 Argon2::default()
58 .hash_password_into(phrase.as_bytes(), b"hashiverse-global-salt", &mut seed)
59 .map_err(|e| anyhow!("error hashing phrase: {}", e))?;
60
61 Self::from_seed(&seed, false)
62 }
63
64 pub fn from_seed(seed: &[u8; 32], skip_pq_commitment_bytes: bool) -> anyhow::Result<Keys> {
65 let signature_key = {
66 let ed25519_seed = blake3::derive_key("hashiverse-pk-ed25519", seed);
67 SignatureKey::from_bytes(&ed25519_seed)?
68 };
69
70 let verification_key = signature_key.verification_key();
71 let verification_key_bytes = verification_key.to_verification_key_bytes();
72 let pq_commitment_bytes = match skip_pq_commitment_bytes {
73 false => pq_commitment_bytes_from_seed(&seed),
74 true => Ok(PQCommitmentBytes::zero())
75 }?;
76
77 Ok(Keys {
78 signature_key,
79 verification_key,
80 verification_key_bytes,
81 pq_commitment_bytes,
82 })
83 }
84
85 pub fn to_persistence(&self, passphrase: &String) -> anyhow::Result<String> {
86 let mut buf = Vec::with_capacity(32 + 32 + 32);
88 buf.extend_from_slice(self.signature_key.as_ref());
89 buf.extend_from_slice(self.verification_key.as_ref());
90 buf.extend_from_slice(self.pq_commitment_bytes.as_ref());
91
92 let mut salt = vec![0u8; 16];
94 tools::random_fill_bytes(&mut salt);
95 let salt_string = SaltString::encode_b64(&salt).map_err(|e| anyhow!("error creating salt: {}", e))?;
96
97 let argon2 = Argon2::default();
98 let hash = argon2
99 .hash_password(passphrase.as_bytes(), &salt_string)
100 .map_err(|e| anyhow!("error hashing passphrase: {}", e))?
101 .hash
102 .ok_or_else(|| anyhow::anyhow!("argon2 failed"))?;
103 let key_bytes = hash.as_bytes();
104 let mut key = Key::default();
105 let copy_len = key_bytes.len().min(key.len());
106 key[..copy_len].copy_from_slice(&key_bytes[..copy_len]);
107
108 let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
110 let nonce_slice = nonce.as_slice();
111
112 let cipher = ChaCha20Poly1305::new(&key);
113 let ciphertext = cipher.encrypt(nonce_slice.into(), buf.as_ref()).map_err(|e| anyhow!("error encrypting buffer: {}", e))?.to_vec();
114
115 let salt_len = salt.len();
117 let nonce_len = nonce_slice.len();
118
119 if salt_len > u8::MAX as usize || nonce_len > u8::MAX as usize {
120 return Err(anyhow!("Salt or nonce too large"));
121 }
122
123 let mut out = Vec::with_capacity(1 + salt_len + 1 + nonce_len + ciphertext.len());
125 out.push(salt_len as u8);
126 out.extend_from_slice(&salt);
127 out.push(nonce_len as u8);
128 out.extend_from_slice(nonce_slice);
129 out.extend_from_slice(&ciphertext);
130
131 Ok(tools::encode_base64(&out))
132 }
133
134 pub fn from_persistence(passphrase: &String, persistence: &str) -> anyhow::Result<Keys> {
135 let decoded = tools::decode_base64(persistence)?;
136
137 if decoded.len() < 2 {
138 return Err(anyhow!("Input too short for salt/nonce lengths"));
139 }
140
141 let salt_len = decoded[0] as usize;
143 if decoded.len() < 1 + salt_len + 1 {
144 return Err(anyhow!("Input too short for salt data"));
145 }
146 let salt_start = 1;
147 let salt_end = salt_start + salt_len;
148 let salt = &decoded[salt_start..salt_end];
149
150 let nonce_len = decoded[salt_end] as usize;
152 let nonce_start = salt_end + 1;
153 let nonce_end = nonce_start + nonce_len;
154 if decoded.len() < nonce_end {
155 return Err(anyhow!("Input too short for nonce data"));
156 }
157 let nonce = &decoded[nonce_start..nonce_end];
158 let ciphertext = &decoded[nonce_end..];
159
160 let salt_string = SaltString::encode_b64(salt).map_err(|e| anyhow!("error creating salt: {}", e))?;
162
163 let argon2 = Argon2::default();
165 let hash = argon2
166 .hash_password(passphrase.as_bytes(), &salt_string)
167 .map_err(|e| anyhow!("error hashing passphrase: {}", e))?
168 .hash
169 .ok_or_else(|| anyhow!("argon2 failed"))?;
170 let key_bytes = hash.as_bytes();
171 let mut key = Key::default();
172 let copy_len = key_bytes.len().min(key.len());
173 key[..copy_len].copy_from_slice(&key_bytes[..copy_len]);
174
175 let cipher = ChaCha20Poly1305::new(&key);
177 let buf = cipher.decrypt(nonce.into(), ciphertext).map_err(|e| anyhow!("Decryption failed: {}", e))?;
178
179 if buf.len() != 32 * 3 {
181 return Err(anyhow!("Decrypted keys len mismatch"));
182 }
183 let signature_key_bytes = <&[u8; 32]>::try_from(&buf[0..32])?;
184 let verification_key_bytes = <&[u8; 32]>::try_from(&buf[32..64])?;
185 let pq_commitment_bytes = <&[u8; 32]>::try_from(&buf[64..96])?;
186
187 let signature_key = SignatureKey::from_bytes(signature_key_bytes)?;
188 let verification_key = VerificationKey::from_bytes_raw(verification_key_bytes)?;
189 let verification_key_bytes = verification_key.to_verification_key_bytes();
190 let pq_commitment_bytes = PQCommitmentBytes::from_slice(pq_commitment_bytes)?;
191
192 Ok(Keys {
193 signature_key,
194 verification_key,
195 verification_key_bytes,
196 pq_commitment_bytes,
197 })
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::string::ToString;
205 use ml_dsa::signature::Keypair;
206 use uuid::Uuid;
207
208 #[tokio::test]
209 async fn test_keys_to_and_from_persistence_roundtrip() -> anyhow::Result<()> {
210 for _ in 0..8 {
211 let passphrase = Uuid::new_v4().to_string();
212
213 let keys = Keys::from_rnd(false)?;
215 let keys_persisted = keys.to_persistence(&passphrase)?;
216 let keys_unpersisted = Keys::from_persistence(&passphrase, &keys_persisted)?;
217
218 assert_eq!(keys.signature_key, keys_unpersisted.signature_key);
219 assert_eq!(keys.verification_key, keys_unpersisted.verification_key);
220 assert_eq!(keys.pq_commitment_bytes, keys_unpersisted.pq_commitment_bytes);
221
222 assert_eq!(keys.signature_key.as_ref(), keys_unpersisted.signature_key.as_ref());
224 assert_eq!(keys.verification_key.as_ref(), keys_unpersisted.verification_key.as_ref());
225 assert_eq!(keys.pq_commitment_bytes.as_ref(), keys_unpersisted.pq_commitment_bytes.as_ref());
226 }
227 Ok(())
228 }
229
230 #[tokio::test]
231 async fn test_pq_keys_are_deterministic_from_seed() {
232 let mut seed = [0u8; 32];
233 tools::random_fill_bytes(&mut seed);
234
235 let keys1 = Keys::from_seed(&seed, false).unwrap();
236 let keys2 = Keys::from_seed(&seed, false).unwrap();
237
238 assert_eq!(
239 keys1.pq_commitment_bytes.as_ref(),
240 keys2.pq_commitment_bytes.as_ref(),
241 "PQ key commitments must be deterministic from the same seed"
242 );
243 }
244
245 #[tokio::test]
246 async fn test_falcon_sign_and_verify() -> anyhow::Result<()> {
247 use falcon_rust::falcon512;
248
249 let mut seed = [0u8; 32];
250 tools::random_fill_bytes(&mut seed);
251
252 let falcon_seed: [u8; 32] = blake3::derive_key("hashiverse-pk-falcon", &seed);
253 let (sk, pk) = falcon512::keygen(falcon_seed);
254
255 let msg = b"hello hashiverse";
256 let sig = falcon512::sign(msg, &sk);
257 assert!(falcon512::verify(msg, &sig, &pk), "Falcon signature should verify");
258
259 let pk_rehydrated = falcon512::PublicKey::from_bytes(&pk.to_bytes())
261 .map_err(|e| anyhow::anyhow!("Failed to decode Falcon public key: {:?}", e))?;
262 assert!(falcon512::verify(msg, &sig, &pk_rehydrated), "Rehydrated Falcon verifying key should verify");
263
264 let sk_rehydrated = falcon512::SecretKey::from_bytes(&sk.to_bytes())
266 .map_err(|e| anyhow::anyhow!("Failed to decode Falcon secret key: {:?}", e))?;
267 let msg2 = b"second message";
268 let sig2 = falcon512::sign(msg2, &sk_rehydrated);
269 assert!(falcon512::verify(msg2, &sig2, &pk), "Rehydrated Falcon signing key should produce valid signatures");
270
271 assert!(!falcon512::verify(b"wrong message", &sig, &pk), "Falcon should reject wrong message");
273
274 Ok(())
275 }
276
277 #[tokio::test]
278 async fn test_dilithium_sign_and_verify() -> anyhow::Result<()> {
279 use ml_dsa::{KeyGen, MlDsa44};
280 use ml_dsa::signature::{Signer, Verifier};
281
282 let mut seed = [0u8; 32];
283 tools::random_fill_bytes(&mut seed);
284 let dilithium_seed = blake3::derive_key("hashiverse-pk-dilithium", &seed);
285
286 let kp = MlDsa44::from_seed(&dilithium_seed.into());
288
289 let msg = b"hello hashiverse";
290 let sig = kp.signing_key().sign(msg);
291
292 assert!(kp.verifying_key().verify(msg, &sig).is_ok(), "Dilithium signature should verify");
294
295 let kp_rehydrated = MlDsa44::from_seed(&dilithium_seed.into());
297 let vk_encoded = kp.verifying_key().encode();
298 let vk_rehydrated_encoded = kp_rehydrated.verifying_key().encode();
299 assert_eq!(vk_encoded, vk_rehydrated_encoded, "Dilithium keys must be identical for the same seed");
300 assert!(
301 kp_rehydrated.verifying_key().verify(msg, &sig).is_ok(),
302 "Rehydrated Dilithium verifying key should verify the same signature"
303 );
304
305 assert!(kp.verifying_key().verify(b"wrong message", &sig).is_err(), "Dilithium should reject wrong message");
307
308 Ok(())
309 }
310
311 #[tokio::test]
312 async fn test_pq_commitment_matches_key() -> anyhow::Result<()> {
313 use falcon_rust::falcon512;
314 use ml_dsa::{KeyGen, MlDsa44};
315
316 let seed = [123u8; 32];
317 let keys = Keys::from_seed(&seed, false)?;
318
319 let expected_falcon: [u8; 16] = {
321 let falcon_seed: [u8; 32] = blake3::derive_key("hashiverse-pk-falcon", &seed);
322 let (_, pk) = falcon512::keygen(falcon_seed);
323 let vrfy_key = pk.to_bytes();
324 let hash = blake3::hash(&vrfy_key);
325 hash.as_bytes()[..16].try_into()?
326 };
327
328 let expected_dilithium: [u8; 16] = {
330 let dilithium_seed = blake3::derive_key("hashiverse-pk-dilithium", &seed);
331 let kp = MlDsa44::from_seed(&dilithium_seed.into());
332 let vk_bytes = kp.verifying_key().encode();
333 let hash = blake3::hash(vk_bytes.as_ref());
334 hash.as_bytes()[..16].try_into()?
335 };
336
337 let expected: Vec<u8> = [expected_falcon, expected_dilithium].concat();
338 assert_eq!(
339 keys.pq_commitment_bytes.as_ref(),
340 expected.as_slice(),
341 "pq_commitment_bytes must match independently computed PQ commitments"
342 );
343
344 Ok(())
345 }
346}