Skip to main content

murk_cli/
crypto.rs

1use std::io::{Read, Write};
2
3use age::x25519::{Identity, Recipient};
4
5/// Errors that can occur during crypto operations.
6#[derive(Debug)]
7pub enum CryptoError {
8    Encrypt(String),
9    Decrypt(String),
10    InvalidKey(String),
11}
12
13impl std::fmt::Display for CryptoError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        match self {
16            CryptoError::Encrypt(msg) => write!(f, "encryption failed: {msg}"),
17            CryptoError::Decrypt(msg) => write!(f, "decryption failed: {msg}"),
18            CryptoError::InvalidKey(msg) => write!(f, "invalid key: {msg}"),
19        }
20    }
21}
22
23/// Parse a public key from its string representation (age1...).
24pub fn parse_recipient(pubkey: &str) -> Result<Recipient, CryptoError> {
25    pubkey
26        .parse::<Recipient>()
27        .map_err(|e| CryptoError::InvalidKey(e.to_string()))
28}
29
30/// Parse a secret key from its string representation (AGE-SECRET-KEY-1...).
31pub fn parse_identity(secret_key: &str) -> Result<Identity, CryptoError> {
32    secret_key
33        .parse::<Identity>()
34        .map_err(|e| CryptoError::InvalidKey(e.to_string()))
35}
36
37/// Encrypt plaintext bytes to one or more recipients.
38pub fn encrypt(plaintext: &[u8], recipients: &[Recipient]) -> Result<Vec<u8>, CryptoError> {
39    let recipient_refs: Vec<&dyn age::Recipient> = recipients
40        .iter()
41        .map(|r| r as &dyn age::Recipient)
42        .collect();
43
44    let encryptor = age::Encryptor::with_recipients(recipient_refs.into_iter())
45        .map_err(|e| CryptoError::Encrypt(e.to_string()))?;
46
47    let mut ciphertext = vec![];
48    let mut writer = encryptor
49        .wrap_output(&mut ciphertext)
50        .map_err(|e| CryptoError::Encrypt(e.to_string()))?;
51
52    writer
53        .write_all(plaintext)
54        .map_err(|e| CryptoError::Encrypt(e.to_string()))?;
55
56    // finish() is critical — without it, the output is silently
57    // truncated and undecryptable. No error, just broken data.
58    writer
59        .finish()
60        .map_err(|e| CryptoError::Encrypt(e.to_string()))?;
61
62    Ok(ciphertext)
63}
64
65/// Decrypt ciphertext using a secret key.
66pub fn decrypt(ciphertext: &[u8], identity: &Identity) -> Result<Vec<u8>, CryptoError> {
67    let decryptor = age::Decryptor::new_buffered(ciphertext)
68        .map_err(|e| CryptoError::Decrypt(e.to_string()))?;
69
70    let mut plaintext = vec![];
71    let mut reader = decryptor
72        .decrypt(std::iter::once(identity as &dyn age::Identity))
73        .map_err(|e| CryptoError::Decrypt(e.to_string()))?;
74
75    reader
76        .read_to_end(&mut plaintext)
77        .map_err(|e| CryptoError::Decrypt(e.to_string()))?;
78
79    Ok(plaintext)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use age::secrecy::ExposeSecret;
86
87    fn generate_keypair() -> (String, String) {
88        let identity = Identity::generate();
89        let secret = identity.to_string();
90        let pubkey = identity.to_public().to_string();
91        (secret.expose_secret().to_string(), pubkey)
92    }
93
94    #[test]
95    fn roundtrip_single_recipient() {
96        let (secret, pubkey) = generate_keypair();
97        let recipient = parse_recipient(&pubkey).unwrap();
98        let identity = parse_identity(&secret).unwrap();
99
100        let plaintext = b"hello darkness";
101        let ciphertext = encrypt(plaintext, &[recipient]).unwrap();
102        let decrypted = decrypt(&ciphertext, &identity).unwrap();
103
104        assert_eq!(decrypted, plaintext);
105    }
106
107    #[test]
108    fn roundtrip_multiple_recipients() {
109        let (secret_a, pubkey_a) = generate_keypair();
110        let (secret_b, pubkey_b) = generate_keypair();
111
112        let recipients = vec![
113            parse_recipient(&pubkey_a).unwrap(),
114            parse_recipient(&pubkey_b).unwrap(),
115        ];
116
117        let plaintext = b"sharing is caring";
118        let ciphertext = encrypt(plaintext, &recipients).unwrap();
119
120        // Both recipients can decrypt
121        let id_a = parse_identity(&secret_a).unwrap();
122        let id_b = parse_identity(&secret_b).unwrap();
123        assert_eq!(decrypt(&ciphertext, &id_a).unwrap(), plaintext);
124        assert_eq!(decrypt(&ciphertext, &id_b).unwrap(), plaintext);
125    }
126
127    #[test]
128    fn wrong_key_fails() {
129        let (_secret, pubkey) = generate_keypair();
130        let (wrong_secret, _) = generate_keypair();
131
132        let recipient = parse_recipient(&pubkey).unwrap();
133        let wrong_identity = parse_identity(&wrong_secret).unwrap();
134
135        let ciphertext = encrypt(b"none of your business", &[recipient]).unwrap();
136        assert!(decrypt(&ciphertext, &wrong_identity).is_err());
137    }
138
139    #[test]
140    fn invalid_key_strings() {
141        assert!(parse_recipient("sine-loco").is_err());
142        assert!(parse_identity("nihil-et-nemo").is_err());
143    }
144
145    // ── New edge-case tests ──
146
147    #[test]
148    fn encrypt_empty_plaintext() {
149        let (secret, pubkey) = generate_keypair();
150        let recipient = parse_recipient(&pubkey).unwrap();
151        let identity = parse_identity(&secret).unwrap();
152
153        let ciphertext = encrypt(b"", &[recipient]).unwrap();
154        let decrypted = decrypt(&ciphertext, &identity).unwrap();
155        assert!(decrypted.is_empty());
156    }
157
158    #[test]
159    fn decrypt_corrupted_ciphertext() {
160        let (secret, _) = generate_keypair();
161        let identity = parse_identity(&secret).unwrap();
162        assert!(decrypt(b"this is not valid ciphertext", &identity).is_err());
163    }
164}