1use thiserror::Error;
13use zeroize::{Zeroize, ZeroizeOnDrop};
14
15use crate::aead::{self, AeadError};
16
17#[derive(Clone, Zeroize, ZeroizeOnDrop)]
20pub struct EncryptionKey([u8; 32]);
21
22impl std::fmt::Debug for EncryptionKey {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.write_str("EncryptionKey(<redacted>)")
25 }
26}
27
28#[derive(Debug, Error)]
30pub enum EncryptionError {
31 #[error("decryption failed")]
32 DecryptionFailed,
33 #[error("invalid key: {0}")]
34 InvalidKey(String),
35 #[error("encryption failed")]
36 EncryptionFailed,
37}
38
39impl From<AeadError> for EncryptionError {
40 fn from(error: AeadError) -> Self {
41 match error {
42 AeadError::Seal => Self::EncryptionFailed,
43 AeadError::Open => Self::DecryptionFailed,
44 }
45 }
46}
47
48pub fn load_key(base64_str: &str) -> Result<EncryptionKey, EncryptionError> {
53 let mut bytes = base64::Engine::decode(
54 &base64::engine::general_purpose::STANDARD,
55 base64_str.trim(),
56 )
57 .map_err(|e| EncryptionError::InvalidKey(format!("base64 decode error: {e}")))?;
58
59 if bytes.len() != 32 {
60 bytes.zeroize();
61 return Err(EncryptionError::InvalidKey(format!(
62 "expected 32 bytes, got {}",
63 bytes.len()
64 )));
65 }
66
67 let mut key = [0u8; 32];
68 key.copy_from_slice(&bytes);
69 bytes.zeroize();
70 Ok(EncryptionKey(key))
71}
72
73#[allow(dead_code)]
78pub fn seal(key: &EncryptionKey, plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>), EncryptionError> {
79 seal_with_aad(key, plaintext, &[])
80}
81
82pub fn seal_with_aad(
83 key: &EncryptionKey,
84 plaintext: &[u8],
85 aad: &[u8],
86) -> Result<(Vec<u8>, Vec<u8>), EncryptionError> {
87 let (ciphertext, nonce_bytes) = aead::seal(&key.0, plaintext, aad)?;
88 Ok((ciphertext, nonce_bytes.to_vec()))
89}
90
91#[allow(dead_code)]
97pub fn open(
98 key: &EncryptionKey,
99 ciphertext: &[u8],
100 nonce: &[u8],
101) -> Result<Vec<u8>, EncryptionError> {
102 open_with_aad(key, ciphertext, nonce, &[])
103}
104
105pub fn open_with_aad(
106 key: &EncryptionKey,
107 ciphertext: &[u8],
108 nonce: &[u8],
109 aad: &[u8],
110) -> Result<Vec<u8>, EncryptionError> {
111 aead::open(&key.0, nonce, ciphertext, aad).map_err(EncryptionError::from)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn test_key() -> EncryptionKey {
119 load_key(&base64::Engine::encode(
120 &base64::engine::general_purpose::STANDARD,
121 [0u8; 32],
122 ))
123 .unwrap()
124 }
125
126 #[test]
127 fn round_trip_plaintext() {
128 let key = test_key();
129 let plaintext = b"hello, world";
130 let (ct, nonce) = seal(&key, plaintext).unwrap();
131 let pt = open(&key, &ct, &nonce).unwrap();
132 assert_eq!(pt, plaintext);
133 }
134
135 #[test]
136 fn wrong_key_fails_decryption() {
137 let key1 = test_key();
138 let key2 = load_key(&base64::Engine::encode(
139 &base64::engine::general_purpose::STANDARD,
140 [1u8; 32],
141 ))
142 .unwrap();
143 let (ct, nonce) = seal(&key1, b"secret").unwrap();
144 assert!(open(&key2, &ct, &nonce).is_err());
145 }
146
147 #[test]
148 fn wrong_nonce_fails_decryption() {
149 let key = test_key();
150 let (ct, _) = seal(&key, b"secret").unwrap();
151 let bad_nonce = vec![0u8; 12];
152 assert!(open(&key, &ct, &bad_nonce).is_err());
153 }
154
155 #[test]
156 fn two_seals_produce_different_nonces() {
157 let key = test_key();
158 let (_, nonce1) = seal(&key, b"same plaintext").unwrap();
159 let (_, nonce2) = seal(&key, b"same plaintext").unwrap();
160 assert_ne!(nonce1, nonce2, "nonce reuse detected");
161 }
162
163 #[test]
164 fn short_key_rejected() {
165 let short = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0u8; 16]);
166 assert!(load_key(&short).is_err());
167 }
168
169 #[test]
170 fn invalid_base64_rejected() {
171 assert!(load_key("not-valid-base64!!!").is_err());
172 }
173
174 #[test]
175 fn key_debug_is_redacted() {
176 let key = test_key();
177 let debug = format!("{key:?}");
178 assert_eq!(debug, "EncryptionKey(<redacted>)");
179 }
180
181 #[test]
182 fn decrypt_failure_maps_to_decryption_failed() {
183 let key = test_key();
184 let (ct, nonce) = seal_with_aad(&key, b"secret", b"alice").unwrap();
185 let err = open_with_aad(&key, &ct, &nonce, b"bob").unwrap_err();
186 assert!(matches!(err, EncryptionError::DecryptionFailed));
187 }
188
189 #[test]
190 fn aad_round_trip_plaintext() {
191 let key = test_key();
192 let aad = b"upstream=test\0subject=alice\0client=soma-client";
193 let plaintext = b"hello, world";
194 let (ct, nonce) = seal_with_aad(&key, plaintext, aad).unwrap();
195 let pt = open_with_aad(&key, &ct, &nonce, aad).unwrap();
196 assert_eq!(pt, plaintext);
197 }
198
199 #[test]
200 fn wrong_aad_fails_decryption() {
201 let key = test_key();
202 let (ct, nonce) = seal_with_aad(&key, b"secret", b"alice").unwrap();
203 assert!(open_with_aad(&key, &ct, &nonce, b"bob").is_err());
204 }
205}