1use aes_gcm::{
13 aead::{rand_core::RngCore, Aead, KeyInit, OsRng},
14 Aes256Gcm, Nonce,
15};
16use sha2::{Digest, Sha256};
17
18const NONCE_LEN: usize = 12;
20
21pub fn key_from_secret(secret: &[u8]) -> [u8; 32] {
23 let mut hasher = Sha256::new();
24 hasher.update(secret);
25 hasher.finalize().into()
26}
27
28pub fn generate_key_hex() -> String {
31 let mut bytes = [0u8; 32];
32 OsRng.fill_bytes(&mut bytes);
33 use std::fmt::Write as _;
34 bytes.iter().fold(String::with_capacity(64), |mut acc, b| {
35 let _ = write!(acc, "{b:02x}");
36 acc
37 })
38}
39
40pub fn encrypt(secret: &[u8], plaintext: &[u8]) -> Vec<u8> {
42 let key = key_from_secret(secret);
43 let cipher = Aes256Gcm::new((&key).into());
44 let mut nonce_bytes = [0u8; NONCE_LEN];
45 OsRng.fill_bytes(&mut nonce_bytes);
46 let ciphertext = cipher
47 .encrypt(&Nonce::from(nonce_bytes), plaintext)
48 .expect("AES-256-GCM encryption never fails for valid keys/nonces");
49 let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
50 out.extend_from_slice(&nonce_bytes);
51 out.extend_from_slice(&ciphertext);
52 out
53}
54
55pub fn decrypt(secret: &[u8], data: &[u8]) -> Option<Vec<u8>> {
59 if data.len() < NONCE_LEN {
60 return None;
61 }
62 let key = key_from_secret(secret);
63 let cipher = Aes256Gcm::new((&key).into());
64 let (nonce_bytes, ciphertext) = data.split_at(NONCE_LEN);
65 let nonce_arr: [u8; NONCE_LEN] = nonce_bytes.try_into().ok()?;
66 cipher.decrypt(&Nonce::from(nonce_arr), ciphertext).ok()
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn round_trips_plaintext() {
75 let secret = b"a-secret-key-base";
76 let blob = encrypt(secret, b"hello world");
77 assert_eq!(decrypt(secret, &blob).as_deref(), Some(&b"hello world"[..]));
78 }
79
80 #[test]
81 fn wrong_secret_fails_to_decrypt() {
82 let blob = encrypt(b"right", b"payload");
83 assert!(decrypt(b"wrong", &blob).is_none());
84 }
85
86 #[test]
87 fn tampered_ciphertext_fails_to_decrypt() {
88 let secret = b"s";
89 let mut blob = encrypt(secret, b"payload");
90 let last = blob.len() - 1;
91 blob[last] ^= 0xff;
92 assert!(decrypt(secret, &blob).is_none());
93 }
94
95 #[test]
96 fn distinct_nonces_produce_distinct_ciphertexts() {
97 let secret = b"s";
98 assert_ne!(encrypt(secret, b"same"), encrypt(secret, b"same"));
99 }
100}