From ee97c6056aeeb784ffef06ad185e75d75b28aa25 Mon Sep 17 00:00:00 2001 From: charles-gauthereau Date: Wed, 8 Jul 2026 20:43:01 +0200 Subject: [PATCH] feat: add decrypt_json_gcm for encrypted status storages --- src/tests/utils/file_tests.rs | 24 ++++++++++++++++++++++++ src/utils/file.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/tests/utils/file_tests.rs b/src/tests/utils/file_tests.rs index 26581b9..697a5d5 100644 --- a/src/tests/utils/file_tests.rs +++ b/src/tests/utils/file_tests.rs @@ -86,3 +86,27 @@ async fn encrypt_stream_starts_with_json_header_line() -> Result<()> { Ok(()) } + +use crate::utils::file::decrypt_json_gcm; + +const VECTOR_KEY_B64: &str = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="; +const VECTOR_ENVELOPE_B64: &str = "AQIDBAUGBwgJCgsM4AfK9w7I2A7UDzMvpJaScfnUYAGDZgPWT5Chrp1pdzMPPQVpNjb6ZEiFea9YdWVFv1UEo9RGmmf+zYUv4I3gE4SU/SBrMwkCHEpJGJOzJtK3tSpJmzLVX3+7EeUNwp4qjZheL8p0pe1x6dRUtx3JmLjz1W/RhWd6zuReDItv6+0jg4CaPOHvFXBreaGNCTRslxbImD+lFBoEOvw8lsbH"; +const VECTOR_PLAINTEXT: &str = "[{\"id\":\"11111111-1111-1111-1111-111111111111\",\"config\":{\"bucket\":\"my-bucket\",\"accessKeyId\":\"AKIA\",\"secretAccessKey\":\"s3cr3t\"},\"provider\":\"s3\"}]"; + +#[test] +fn decrypt_json_gcm_decrypts_node_vector() { + let plaintext = decrypt_json_gcm(VECTOR_ENVELOPE_B64, VECTOR_KEY_B64).unwrap(); + assert_eq!(String::from_utf8(plaintext).unwrap(), VECTOR_PLAINTEXT); +} + +#[test] +fn decrypt_json_gcm_rejects_wrong_key() { + let wrong_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + assert!(decrypt_json_gcm(VECTOR_ENVELOPE_B64, wrong_key).is_err()); +} + +#[test] +fn decrypt_json_gcm_rejects_short_input() { + // 8 bytes base64 -> shorter than nonce(12)+tag(16) + assert!(decrypt_json_gcm("AAAAAAAAAAA=", VECTOR_KEY_B64).is_err()); +} diff --git a/src/utils/file.rs b/src/utils/file.rs index 2cfc31d..13f0ea2 100644 --- a/src/utils/file.rs +++ b/src/utils/file.rs @@ -175,3 +175,32 @@ pub async fn decrypt_file_stream_gcm( writer.flush().await?; Ok(()) } + +/// Decrypt a base64 `nonce(12) ‖ ciphertext ‖ tag(16)` AES-256-GCM envelope +/// using the raw master key (STANDARD base64). Returns the plaintext bytes. +pub fn decrypt_json_gcm(ciphertext_b64: &str, master_key_b64: &str) -> Result> { + let master_key_bytes = general_purpose::STANDARD + .decode(master_key_b64) + .map_err(|_| anyhow::anyhow!("Invalid base64 master key"))?; + + let data = general_purpose::STANDARD + .decode(ciphertext_b64) + .map_err(|_| anyhow::anyhow!("Invalid base64 ciphertext"))?; + + if data.len() < 12 + 16 { + return Err(anyhow::anyhow!("Ciphertext too short")); + } + + let key = Key::::try_from(master_key_bytes.as_slice()) + .map_err(|_| anyhow::anyhow!("Invalid AES-256 key length"))?; + let cipher = Aes256Gcm::new(&key); + + let nonce = Nonce::try_from(&data[..12]) + .map_err(|_| anyhow::anyhow!("Invalid nonce length"))?; + + let plaintext = cipher + .decrypt(&nonce, &data[12..]) + .map_err(|e| anyhow::anyhow!("AES-GCM decryption failed: {:?}", e))?; + + Ok(plaintext) +}