@@ -2295,3 +2295,66 @@ async fn snapshot_hook_fires_with_store() {
22952295 . expect ( "hook present when a store is set" ) ;
22962296 hook ( ) . await ;
22972297}
2298+
2299+ /// A KMS hook that "encrypts" by prefixing `ENC:` and "decrypts" by stripping
2300+ /// it, so a test can prove a read path returns plaintext, not the stored
2301+ /// ciphertext.
2302+ struct PrefixKmsHook ;
2303+ impl fakecloud_core:: delivery:: KmsHook for PrefixKmsHook {
2304+ fn encrypt (
2305+ & self ,
2306+ _account_id : & str ,
2307+ _region : & str ,
2308+ _key_id : & str ,
2309+ plaintext : & [ u8 ] ,
2310+ _service_principal : & str ,
2311+ _ctx : std:: collections:: HashMap < String , String > ,
2312+ ) -> Result < String , String > {
2313+ Ok ( format ! ( "ENC:{}" , String :: from_utf8_lossy( plaintext) ) )
2314+ }
2315+ fn decrypt (
2316+ & self ,
2317+ _account_id : & str ,
2318+ ciphertext_b64 : & str ,
2319+ _service_principal : & str ,
2320+ _ctx : std:: collections:: HashMap < String , String > ,
2321+ ) -> Result < Vec < u8 > , String > {
2322+ Ok ( ciphertext_b64
2323+ . strip_prefix ( "ENC:" )
2324+ . unwrap_or ( ciphertext_b64)
2325+ . as_bytes ( )
2326+ . to_vec ( ) )
2327+ }
2328+ }
2329+
2330+ #[ tokio:: test]
2331+ async fn batch_get_secret_value_returns_plaintext_not_ciphertext ( ) {
2332+ // GetSecretValue decrypts a KMS-backed secret, but BatchGetSecretValue
2333+ // pushed the raw stored ciphertext -- so a batch fetch returned an
2334+ // unusable encrypted blob (bug-audit 2026-06-20, 1.10).
2335+ let state = make_state ( ) ;
2336+ let svc = SecretsManagerService :: new ( state) . with_kms_hook ( std:: sync:: Arc :: new ( PrefixKmsHook ) ) ;
2337+
2338+ let req = make_request (
2339+ "CreateSecret" ,
2340+ r#"{"Name":"enc-secret","SecretString":"topsecret","KmsKeyId":"alias/test"}"# ,
2341+ ) ;
2342+ svc. handle ( req) . await . unwrap ( ) ;
2343+
2344+ // Sanity: single-get decrypts.
2345+ let req = make_request ( "GetSecretValue" , r#"{"SecretId":"enc-secret"}"# ) ;
2346+ let single: Value =
2347+ serde_json:: from_slice ( svc. handle ( req) . await . unwrap ( ) . body . expect_bytes ( ) ) . unwrap ( ) ;
2348+ assert_eq ! ( single[ "SecretString" ] , "topsecret" ) ;
2349+
2350+ // The fix: batch must decrypt too.
2351+ let body = serde_json:: json!( { "SecretIdList" : [ "enc-secret" ] } ) ;
2352+ let req = make_request ( "BatchGetSecretValue" , & body. to_string ( ) ) ;
2353+ let batch: Value =
2354+ serde_json:: from_slice ( svc. handle ( req) . await . unwrap ( ) . body . expect_bytes ( ) ) . unwrap ( ) ;
2355+ let v = & batch[ "SecretValues" ] . as_array ( ) . unwrap ( ) [ 0 ] ;
2356+ assert_eq ! (
2357+ v[ "SecretString" ] , "topsecret" ,
2358+ "BatchGetSecretValue must return plaintext, not ciphertext"
2359+ ) ;
2360+ }
0 commit comments