1use age::x25519::{Identity, Recipient};
12use ring::rand::SecureRandom;
13use std::fs;
14use std::path::Path;
15use std::str::FromStr;
16
17use crate::config::identity_path;
18use crate::error::{PwrError, Result};
19
20pub fn generate_psk() -> [u8; 32] {
29 let rng = ring::rand::SystemRandom::new();
30 let mut key = [0u8; 32];
31 rng.fill(&mut key).expect("CSPRNG failure: cannot generate PSK");
32 key
33}
34
35fn decode_hex(s: &str) -> std::result::Result<Vec<u8>, String> {
37 if s.len() % 2 != 0 {
38 return Err("hex string must have even length".into());
39 }
40 (0..s.len())
41 .step_by(2)
42 .map(|i| {
43 u8::from_str_radix(&s[i..i + 2], 16)
44 .map_err(|e| format!("invalid hex at position {}: {}", i, e))
45 })
46 .collect()
47}
48
49fn encode_hex(bytes: &[u8]) -> String {
51 bytes.iter().map(|b| format!("{:02x}", b)).collect()
52}
53
54pub fn psk_from_hex(hex_str: &str) -> Result<[u8; 32]> {
56 let bytes = decode_hex(hex_str)
57 .map_err(|e| PwrError::Crypto(format!("invalid PSK hex: {}", e)))?;
58 if bytes.len() != 32 {
59 return Err(PwrError::Crypto(format!(
60 "PSK must be 32 bytes, got {}",
61 bytes.len()
62 )));
63 }
64 let mut key = [0u8; 32];
65 key.copy_from_slice(&bytes);
66 Ok(key)
67}
68
69pub fn psk_to_hex(key: &[u8; 32]) -> String {
71 encode_hex(key)
72}
73
74const AUTH_CONTEXT: &[u8] = b"pwr-auth-v1";
76
77pub fn compute_client_proof(psk: &[u8; 32], client_nonce: &[u8; 32]) -> [u8; 32] {
81 let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, psk);
82 let mut ctx = ring::hmac::Context::with_key(&key);
83 ctx.update(client_nonce);
84 ctx.update(AUTH_CONTEXT);
85 let tag = ctx.sign();
86 let mut proof = [0u8; 32];
87 proof.copy_from_slice(tag.as_ref());
88 proof
89}
90
91pub fn compute_server_proof(
97 psk: &[u8; 32],
98 client_nonce: &[u8; 32],
99 server_nonce: &[u8; 32],
100) -> [u8; 32] {
101 let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, psk);
102 let mut ctx = ring::hmac::Context::with_key(&key);
103 ctx.update(client_nonce);
104 ctx.update(server_nonce);
105 ctx.update(AUTH_CONTEXT);
106 let tag = ctx.sign();
107 let mut proof = [0u8; 32];
108 proof.copy_from_slice(tag.as_ref());
109 proof
110}
111
112pub fn generate_age_identity() -> Result<(Identity, String)> {
122 let identity = Identity::generate();
123 let public_key = identity.to_public().to_string();
124
125 let path = identity_path();
126 if let Some(parent) = path.parent() {
127 fs::create_dir_all(parent)?;
128 }
129
130 use age::secrecy::ExposeSecret;
132 let secret_str = identity.to_string();
133 let exposed: &str = secret_str.expose_secret();
134 fs::write(&path, exposed.as_bytes())?;
135
136 #[cfg(unix)]
138 {
139 use std::os::unix::fs::PermissionsExt;
140 let mut perms = fs::metadata(&path)?.permissions();
141 perms.set_mode(0o600);
142 fs::set_permissions(&path, perms)?;
143 }
144
145 log::info!("Age identity saved to {}", path.display());
146 Ok((identity, public_key))
147}
148
149pub fn load_age_identity() -> Result<Identity> {
154 let path = identity_path();
155 if !path.exists() {
156 return Err(PwrError::Crypto(
157 "No age identity found. Run 'pwr init' to generate one.".into(),
158 ));
159 }
160
161 let contents = fs::read_to_string(&path)?;
162 let contents = contents.trim();
163
164 Identity::from_str(contents)
165 .map_err(|e| PwrError::Crypto(format!("Failed to parse age identity: {}", e)))
166}
167
168pub fn age_identity_exists() -> bool {
170 identity_path().exists()
171}
172
173pub fn age_encrypt(plaintext: &[u8], public_key: &str) -> Result<Vec<u8>> {
182 let recipient = Recipient::from_str(public_key)
183 .map_err(|e| PwrError::Crypto(format!("Invalid age public key: {}", e)))?;
184
185 let encryptor = age::Encryptor::with_recipients([&recipient as &dyn age::Recipient].into_iter())
186 .map_err(|e| PwrError::Crypto(format!("Age encrypt setup failed: {}", e)))?;
187
188 let mut encrypted = Vec::new();
189 let mut writer = encryptor
190 .wrap_output(&mut encrypted)
191 .map_err(|e| PwrError::Crypto(format!("Age encrypt wrap failed: {}", e)))?;
192
193 std::io::copy(&mut &plaintext[..], &mut writer)
194 .map_err(|e| PwrError::Crypto(format!("Age encrypt write failed: {}", e)))?;
195
196 writer
197 .finish()
198 .map_err(|e| PwrError::Crypto(format!("Age encrypt finish failed: {}", e)))?;
199
200 Ok(encrypted)
201}
202
203pub fn age_decrypt(encrypted: &[u8], identity: &Identity) -> Result<Vec<u8>> {
209 let decrypted = age::decrypt(identity, encrypted)
211 .map_err(|e| PwrError::Crypto(format!("Age decryption failed: {}", e)))?;
212
213 Ok(decrypted)
214}
215
216pub fn derive_project_key(psk: &[u8; 32], project_uuid: &uuid::Uuid) -> [u8; 32] {
230 use ring::hkdf::{Salt, HKDF_SHA256};
231
232 let salt = Salt::new(HKDF_SHA256, psk);
234 let info = project_uuid.as_bytes();
236
237 let mut derived = [0u8; 32];
239 salt.extract(&[])
240 .expand(&[info], HKDF_SHA256)
241 .expect("HKDF-Expand failure")
242 .fill(&mut derived)
243 .expect("HKDF fill failure: output length exceeds limit");
244
245 derived
246}
247
248pub fn sha256_hex(data: &[u8]) -> String {
255 crate::integrity::hash_bytes(data)
256}
257
258pub fn sha256_file(path: &Path) -> Result<String> {
261 crate::integrity::hash_file(path)
262}
263
264#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
274 fn test_generate_psk_is_random() {
275 let psk1 = generate_psk();
276 let psk2 = generate_psk();
277 assert_ne!(psk1, psk2);
278 assert_eq!(psk1.len(), 32);
279 }
280
281 #[test]
282 fn test_psk_hex_round_trip() {
283 let psk = generate_psk();
284 let hex = psk_to_hex(&psk);
285 let decoded = psk_from_hex(&hex).unwrap();
286 assert_eq!(psk, decoded);
287 }
288
289 #[test]
290 fn test_psk_from_hex_rejects_short() {
291 assert!(psk_from_hex("abcdef").is_err());
292 }
293
294 #[test]
295 fn test_client_proof_deterministic() {
296 let psk = [0x42; 32];
297 let nonce = [0x99; 32];
298 let proof1 = compute_client_proof(&psk, &nonce);
299 let proof2 = compute_client_proof(&psk, &nonce);
300 assert_eq!(proof1, proof2);
301 }
302
303 #[test]
304 fn test_client_and_server_proofs_differ() {
305 let psk = [0x42; 32];
306 let c_nonce = [0x11; 32];
307 let s_nonce = [0x22; 32];
308
309 let client_proof = compute_client_proof(&psk, &c_nonce);
310 let server_proof = compute_server_proof(&psk, &c_nonce, &s_nonce);
311 assert_ne!(client_proof, server_proof);
312 }
313
314 #[test]
316 fn test_age_encrypt_decrypt_round_trip() -> Result<()> {
317 let identity = Identity::generate();
318 let public_key = identity.to_public().to_string();
319
320 let plaintext = b"Project archive contents - this must remain confidential at rest.";
321 let encrypted = age_encrypt(plaintext, &public_key)?;
322 let decrypted = age_decrypt(&encrypted, &identity)?;
323
324 assert_eq!(decrypted, plaintext);
325 assert_ne!(encrypted, plaintext);
326 Ok(())
327 }
328
329 #[test]
330 fn test_age_encrypt_empty() -> Result<()> {
331 let identity = Identity::generate();
332 let public_key = identity.to_public().to_string();
333
334 let encrypted = age_encrypt(b"", &public_key)?;
335 let decrypted = age_decrypt(&encrypted, &identity)?;
336 assert!(decrypted.is_empty());
337 Ok(())
338 }
339
340 #[test]
341 fn test_age_decrypt_wrong_identity_fails() -> Result<()> {
342 let id1 = Identity::generate();
343 let id2 = Identity::generate();
344 let public_key = id1.to_public().to_string();
345
346 let encrypted = age_encrypt(b"secret message", &public_key)?;
347 let result = age_decrypt(&encrypted, &id2);
348 assert!(result.is_err());
349 Ok(())
350 }
351
352 #[test]
354 fn test_sha256_hex_known() {
355 assert_eq!(
356 sha256_hex(b"hello world"),
357 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
358 );
359 }
360
361 #[test]
362 fn test_sha256_file() -> Result<()> {
363 let tmp = tempfile::TempDir::new()?;
364 let path = tmp.path().join("test.txt");
365 fs::write(&path, b"hello world")?;
366
367 let hash = sha256_file(&path)?;
368 assert_eq!(
369 hash,
370 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
371 );
372 Ok(())
373 }
374
375 #[test]
377 fn test_derive_project_key_deterministic() {
378 let psk = [0x42; 32];
379 let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
380
381 let key1 = derive_project_key(&psk, &uuid);
382 let key2 = derive_project_key(&psk, &uuid);
383 assert_eq!(key1, key2);
384 }
385
386 #[test]
387 fn test_derive_project_key_different_uuids_produce_different_keys() {
388 let psk = [0x42; 32];
389 let uuid1 = uuid::Uuid::new_v4();
390 let uuid2 = uuid::Uuid::new_v4();
391
392 let key1 = derive_project_key(&psk, &uuid1);
393 let key2 = derive_project_key(&psk, &uuid2);
394 assert_ne!(key1, key2);
395 }
396
397 #[test]
398 fn test_derive_project_key_different_psks_produce_different_keys() {
399 let psk1 = [0x11; 32];
400 let psk2 = [0x22; 32];
401 let uuid = uuid::Uuid::new_v4();
402
403 let key1 = derive_project_key(&psk1, &uuid);
404 let key2 = derive_project_key(&psk2, &uuid);
405 assert_ne!(key1, key2);
406 }
407
408 #[test]
409 fn test_derive_project_key_output_length() {
410 let psk = generate_psk();
411 let uuid = uuid::Uuid::new_v4();
412 let key = derive_project_key(&psk, &uuid);
413 assert_eq!(key.len(), 32);
414 }
415}