Skip to main content

agent_first_pay/rpc/
crypto.rs

1use aes_gcm::aead::{Aead, OsRng};
2use aes_gcm::{AeadCore, Aes256Gcm, KeyInit};
3use sha2::{Digest, Sha256};
4
5pub struct Cipher {
6    key: [u8; 32],
7}
8
9impl Cipher {
10    /// Derive a 32-byte AES-256 key from an arbitrary secret string via SHA-256.
11    pub fn from_secret(secret: &str) -> Self {
12        let hash = Sha256::digest(secret.as_bytes());
13        let mut key = [0u8; 32];
14        key.copy_from_slice(&hash);
15        Self { key }
16    }
17
18    /// Encrypt plaintext: zstd compress → AES-256-GCM encrypt. Returns `(nonce, ciphertext)`.
19    pub fn encrypt(&self, plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
20        let compressed =
21            zstd::bulk::compress(plaintext, 1).map_err(|e| format!("zstd compress: {e}"))?;
22        let cipher =
23            Aes256Gcm::new_from_slice(&self.key).map_err(|e| format!("cipher init: {e}"))?;
24        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
25        let ciphertext = cipher
26            .encrypt(&nonce, compressed.as_slice())
27            .map_err(|e| format!("encrypt: {e}"))?;
28        Ok((nonce.to_vec(), ciphertext))
29    }
30
31    /// Decrypt ciphertext: AES-256-GCM decrypt → zstd decompress.
32    pub fn decrypt(&self, nonce: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, String> {
33        let cipher =
34            Aes256Gcm::new_from_slice(&self.key).map_err(|e| format!("cipher init: {e}"))?;
35        let nonce = aes_gcm::Nonce::from_slice(nonce);
36        let compressed = cipher
37            .decrypt(nonce, ciphertext)
38            .map_err(|e| format!("decrypt: {e}"))?;
39        // 64 MiB decompression cap to prevent zip-bomb DoS
40        zstd::bulk::decompress(&compressed, 64 * 1024 * 1024)
41            .map_err(|e| format!("zstd decompress: {e}"))
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn roundtrip() {
51        let cipher = Cipher::from_secret("test-password");
52        let plaintext = b"hello world";
53        let (nonce, ct) = cipher.encrypt(plaintext).ok().unwrap(); // test-only
54        let decrypted = cipher.decrypt(&nonce, &ct).ok().unwrap(); // test-only
55        assert_eq!(decrypted, plaintext);
56    }
57
58    #[test]
59    fn wrong_key_fails() {
60        let c1 = Cipher::from_secret("key-a");
61        let c2 = Cipher::from_secret("key-b");
62        let (nonce, ct) = c1.encrypt(b"secret").ok().unwrap(); // test-only
63        assert!(c2.decrypt(&nonce, &ct).is_err());
64    }
65}