1use rainier_support::setting_enum;
55
56setting_enum! {
57 pub enum CryptScheme: "encryption scheme" {
71 #[default]
77 Native = "native",
78
79 Php = "php",
83 }
84}
85
86use aes::cipher::block_padding::Pkcs7;
87use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
88use base64::engine::general_purpose::STANDARD as B64;
89use base64::Engine as _;
90use hmac::{Hmac, Mac};
91use rainier_support::{Error, Result};
92use serde::{Deserialize, Serialize};
93use sha2::Sha256;
94use subtle::ConstantTimeEq;
95
96use crate::encrypter::Encrypter;
97use crate::key::{Key, KeyRing};
98
99type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
100type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
101type HmacSha256 = Hmac<Sha256>;
102
103const IV_LEN: usize = 16;
105
106#[derive(Debug, Serialize, Deserialize)]
108struct Payload {
109 iv: String,
110 value: String,
111 mac: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
116 tag: Option<String>,
117}
118
119pub struct PhpEncrypter {
121 keys: KeyRing,
122}
123
124impl PhpEncrypter {
125 pub fn new(keys: KeyRing) -> Self {
131 Self { keys }
132 }
133
134 pub fn keys(&self) -> &KeyRing {
136 &self.keys
137 }
138
139 fn encrypt_with(key: &Key, plain: &[u8]) -> Result<String> {
141 let mut iv = [0u8; IV_LEN];
142 rand::Rng::fill(&mut rand::thread_rng(), &mut iv[..]);
143
144 let ciphertext = Aes256CbcEnc::new_from_slices(key.bytes(), &iv)
145 .map_err(|_| Error::internal("the key or IV is the wrong length for AES-256-CBC"))?
146 .encrypt_padded_vec_mut::<Pkcs7>(plain);
147
148 let iv = B64.encode(iv);
149 let value = B64.encode(&ciphertext);
150 let mac = Self::mac(key, &iv, &value)?;
151
152 let payload = serde_json::to_vec(&Payload { iv, value, mac, tag: None })?;
153 Ok(B64.encode(payload))
154 }
155
156 fn decrypt_with(key: &Key, payload: &Payload) -> Result<Vec<u8>> {
158 let expected = Self::mac(key, &payload.iv, &payload.value)?;
159
160 let presented = hex_decode(&payload.mac).ok_or_else(invalid)?;
163 let expected = hex_decode(&expected).ok_or_else(invalid)?;
164
165 if !bool::from(presented.ct_eq(&expected)) {
166 return Err(invalid());
167 }
168
169 let iv = B64.decode(&payload.iv).map_err(|_| invalid())?;
170 let ciphertext = B64.decode(&payload.value).map_err(|_| invalid())?;
171
172 Aes256CbcDec::new_from_slices(key.bytes(), &iv)
173 .map_err(|_| invalid())?
174 .decrypt_padded_vec_mut::<Pkcs7>(&ciphertext)
175 .map_err(|_| invalid())
176 }
177
178 fn mac(key: &Key, iv: &str, value: &str) -> Result<String> {
184 let mut mac = HmacSha256::new_from_slice(key.bytes())
185 .map_err(|_| Error::internal("the key is the wrong length for HMAC-SHA256"))?;
186
187 mac.update(iv.as_bytes());
188 mac.update(value.as_bytes());
189
190 Ok(hex_encode(&mac.finalize().into_bytes()))
191 }
192}
193
194impl Encrypter for PhpEncrypter {
195 fn encrypt_bytes(&self, plain: &[u8]) -> Result<String> {
196 Self::encrypt_with(self.keys.current(), plain)
197 }
198
199 fn decrypt_bytes(&self, payload: &str) -> Result<Vec<u8>> {
200 let decoded = B64.decode(payload).map_err(|_| invalid())?;
201 let payload: Payload = serde_json::from_slice(&decoded).map_err(|_| invalid())?;
202
203 for key in self.keys.all() {
208 if let Ok(plain) = Self::decrypt_with(key, &payload) {
209 return Ok(plain);
210 }
211 }
212
213 Err(invalid())
214 }
215}
216
217impl std::fmt::Debug for PhpEncrypter {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("PhpEncrypter").field("keys", &self.keys.ids()).finish()
220 }
221}
222
223fn invalid() -> Error {
226 Error::internal("could not decrypt the payload")
227}
228
229fn hex_encode(bytes: &[u8]) -> String {
230 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
231}
232
233fn hex_decode(hex: &str) -> Option<Vec<u8>> {
234 if !hex.len().is_multiple_of(2) {
235 return None;
236 }
237
238 (0..hex.len()).step_by(2).map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()).collect()
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::encrypter::EncrypterExt;
245
246 fn encrypter() -> PhpEncrypter {
247 PhpEncrypter::new(KeyRing::new(Key::generate()))
248 }
249
250 #[test]
251 fn a_value_round_trips() {
252 let encrypter = encrypter();
253 let payload = encrypter.encrypt("a card number").unwrap();
254
255 assert_eq!(encrypter.decrypt(&payload).unwrap(), "a card number");
256 }
257
258 #[test]
259 fn the_payload_is_the_shape_php_writes() {
260 let payload = encrypter().encrypt("hello").unwrap();
261
262 let decoded = B64.decode(&payload).expect("base64 around the JSON");
263 let json: serde_json::Value = serde_json::from_slice(&decoded).expect("JSON inside");
264
265 assert!(json["iv"].is_string());
266 assert!(json["value"].is_string());
267 assert!(json["mac"].is_string());
268
269 assert_eq!(B64.decode(json["iv"].as_str().unwrap()).unwrap().len(), IV_LEN);
272 assert_eq!(json["mac"].as_str().unwrap().len(), 64);
274 }
275
276 #[test]
277 fn the_mac_covers_the_base64_forms() {
278 let key = Key::generate();
282 let iv = B64.encode([7u8; IV_LEN]);
283 let value = B64.encode(b"ciphertext");
284
285 let mut expected = HmacSha256::new_from_slice(key.bytes()).unwrap();
286 expected.update(format!("{iv}{value}").as_bytes());
287
288 assert_eq!(
289 PhpEncrypter::mac(&key, &iv, &value).unwrap(),
290 hex_encode(&expected.finalize().into_bytes())
291 );
292 }
293
294 #[test]
295 fn a_tampered_ciphertext_is_refused_rather_than_decrypted() {
296 let encrypter = encrypter();
298 let payload = encrypter.encrypt("a card number").unwrap();
299
300 let decoded = B64.decode(&payload).unwrap();
301 let mut json: Payload = serde_json::from_slice(&decoded).unwrap();
302
303 let mut ciphertext = B64.decode(&json.value).unwrap();
305 ciphertext[0] ^= 0xff;
306 json.value = B64.encode(&ciphertext);
307
308 let tampered = B64.encode(serde_json::to_vec(&json).unwrap());
309
310 assert!(encrypter.decrypt(&tampered).is_err());
311 }
312
313 #[test]
314 fn a_tampered_iv_is_refused() {
315 let encrypter = encrypter();
316 let payload = encrypter.encrypt("a card number").unwrap();
317
318 let decoded = B64.decode(&payload).unwrap();
319 let mut json: Payload = serde_json::from_slice(&decoded).unwrap();
320
321 let mut iv = B64.decode(&json.iv).unwrap();
322 iv[0] ^= 0xff;
323 json.iv = B64.encode(&iv);
324
325 let tampered = B64.encode(serde_json::to_vec(&json).unwrap());
326
327 assert!(encrypter.decrypt(&tampered).is_err());
328 }
329
330 #[test]
331 fn another_applications_payload_does_not_decrypt() {
332 let payload = encrypter().encrypt("a card number").unwrap();
333
334 assert!(encrypter().decrypt(&payload).is_err());
335 }
336
337 #[test]
338 fn a_retired_key_still_reads_what_it_wrote() {
339 let old = Key::generate();
343 let payload = PhpEncrypter::new(KeyRing::new(old.clone())).encrypt("secret").unwrap();
344
345 let rotated = PhpEncrypter::new(KeyRing::new(Key::generate()).with_previous(old));
346 assert_eq!(rotated.decrypt(&payload).unwrap(), "secret");
347 }
348
349 #[test]
350 fn nonsense_is_an_error_rather_than_a_panic() {
351 let encrypter = encrypter();
352
353 for payload in ["", "not base64 at all !!", &B64.encode("not json"), &B64.encode("{}")] {
354 assert!(encrypter.decrypt(payload).is_err(), "{payload:?}");
355 }
356 }
357
358 #[test]
359 fn every_failure_looks_the_same() {
360 let encrypter = encrypter();
363 let payload = encrypter.encrypt("secret").unwrap();
364
365 let corrupt = B64.encode("{}");
366 let wrong_key =
367 PhpEncrypter::new(KeyRing::new(Key::generate())).encrypt("secret").unwrap();
368 let _ = payload;
369
370 assert_eq!(
371 encrypter.decrypt(&corrupt).unwrap_err().message(),
372 encrypter.decrypt(&wrong_key).unwrap_err().message()
373 );
374 }
375
376 #[test]
377 fn empty_and_long_values_round_trip() {
378 let encrypter = encrypter();
379
380 for plain in ["", "a", &"x".repeat(10_000), "héllo — ünicode"] {
381 assert_eq!(encrypter.decrypt(&encrypter.encrypt(plain).unwrap()).unwrap(), plain);
382 }
383 }
384
385 #[test]
386 fn hex_round_trips() {
387 assert_eq!(hex_decode(&hex_encode(&[0, 15, 16, 255])), Some(vec![0, 15, 16, 255]));
388 assert_eq!(hex_decode("abc"), None, "an odd length is not hex");
389 assert_eq!(hex_decode("zz"), None);
390 }
391
392 fn fixed_key() -> Key {
394 Key::from_base64("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=").unwrap()
395 }
396
397 #[test]
398 fn the_ciphertext_is_pinned_to_a_known_vector() {
399 let key = fixed_key();
409 let iv = B64.encode([0u8; IV_LEN]);
410
411 let ciphertext = Aes256CbcEnc::new_from_slices(key.bytes(), &[0u8; IV_LEN])
412 .unwrap()
413 .encrypt_padded_vec_mut::<Pkcs7>(b"a card number");
414
415 assert_eq!(B64.encode(&ciphertext), "nH4xXUTQpgBTKXvgzml9eg==");
416
417 assert_eq!(
420 PhpEncrypter::mac(&key, &iv, &B64.encode(&ciphertext)).unwrap(),
421 "f648b5eac55d1dd21fe92175da4660271c245c3c0d23a9839e932eb583716aad"
422 );
423 }
424}