Skip to main content

agent_vault/core/
crypto.rs

1use std::io::{Read, Write};
2
3use secrecy::{ExposeSecret, SecretString};
4
5use crate::error::VaultError;
6
7/// Generate a new age X25519 keypair. Returns (secret_key_string, public_key_string).
8pub fn generate_keypair() -> (SecretString, String) {
9    let identity = age::x25519::Identity::generate();
10    let secret = identity.to_string();
11    let public = identity.to_public().to_string();
12    (SecretString::from(secret.expose_secret().to_string()), public)
13}
14
15/// Parse an age identity (private key) from its string representation.
16pub fn parse_identity(key_str: &str) -> Result<age::x25519::Identity, VaultError> {
17    key_str
18        .parse::<age::x25519::Identity>()
19        .map_err(|e| VaultError::AgeKey(e.to_string()))
20}
21
22/// Parse an age recipient (public key) from its string representation.
23pub fn parse_recipient(key_str: &str) -> Result<age::x25519::Recipient, VaultError> {
24    key_str
25        .parse::<age::x25519::Recipient>()
26        .map_err(|e| VaultError::AgeKey(e.to_string()))
27}
28
29/// Encrypt plaintext for multiple recipients. Returns the encrypted bytes.
30pub fn encrypt(plaintext: &[u8], recipients: &[age::x25519::Recipient]) -> Result<Vec<u8>, VaultError> {
31    let encryptor = age::Encryptor::with_recipients(
32        recipients.iter().map(|r| r as &dyn age::Recipient),
33    )
34    .map_err(|e| VaultError::AgeEncrypt(e.to_string()))?;
35
36    let mut encrypted = vec![];
37    let mut writer = encryptor
38        .wrap_output(&mut encrypted)
39        .map_err(|e| VaultError::AgeEncrypt(e.to_string()))?;
40    writer
41        .write_all(plaintext)
42        .map_err(|e| VaultError::AgeEncrypt(e.to_string()))?;
43    writer
44        .finish()
45        .map_err(|e| VaultError::AgeEncrypt(e.to_string()))?;
46
47    Ok(encrypted)
48}
49
50/// Decrypt ciphertext using an identity. Returns the plaintext as a SecretString.
51pub fn decrypt(ciphertext: &[u8], identity: &age::x25519::Identity) -> Result<SecretString, VaultError> {
52    let decryptor = age::Decryptor::new(ciphertext)
53        .map_err(|e| VaultError::AgeDecrypt(e.to_string()))?;
54
55    let mut reader = decryptor
56        .decrypt(std::iter::once(identity as &dyn age::Identity))
57        .map_err(|e| VaultError::AgeDecrypt(e.to_string()))?;
58
59    let mut plaintext = String::new();
60    reader
61        .read_to_string(&mut plaintext)
62        .map_err(|e| VaultError::AgeDecrypt(e.to_string()))?;
63
64    Ok(SecretString::from(plaintext))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_roundtrip() {
73        let (secret, public) = generate_keypair();
74        let recipient = parse_recipient(&public).unwrap();
75        let identity = parse_identity(secret.expose_secret()).unwrap();
76
77        let plaintext = b"hello world";
78        let ciphertext = encrypt(plaintext, &[recipient]).unwrap();
79        let decrypted = decrypt(&ciphertext, &identity).unwrap();
80        assert_eq!(decrypted.expose_secret(), "hello world");
81    }
82
83    #[test]
84    fn test_multi_recipient() {
85        let (secret1, public1) = generate_keypair();
86        let (secret2, public2) = generate_keypair();
87        let r1 = parse_recipient(&public1).unwrap();
88        let r2 = parse_recipient(&public2).unwrap();
89        let id1 = parse_identity(secret1.expose_secret()).unwrap();
90        let id2 = parse_identity(secret2.expose_secret()).unwrap();
91
92        let ciphertext = encrypt(b"multi", &[r1, r2]).unwrap();
93        assert_eq!(decrypt(&ciphertext, &id1).unwrap().expose_secret(), "multi");
94        assert_eq!(decrypt(&ciphertext, &id2).unwrap().expose_secret(), "multi");
95    }
96}