feat(kms): AWS-shaped binary ciphertext blob with AES-256-GCM#766
Merged
vieiralucas merged 2 commits intomainfrom Apr 25, 2026
Merged
feat(kms): AWS-shaped binary ciphertext blob with AES-256-GCM#766vieiralucas merged 2 commits intomainfrom
vieiralucas merged 2 commits intomainfrom
Conversation
Replaces the leaky textual envelope (`fakecloud-kms:<key>:<base64-plaintext>`) with an AWS-compatible opaque binary ciphertext blob shaped as: version (4) | key-id len (8) | key-id (N) | IV (12) | ct-len (4) | ciphertext (M) | AES-GCM tag (16) Encrypted with AES-256-GCM under a per-account 32-byte master key generated lazily and persisted in `KmsState` (so ciphertexts produced before a server restart still decrypt afterwards). Anyone inspecting the ciphertext bytes sees binary structure, not the plaintext. - New module `fakecloud-kms::blob` with `encode` / `decode` and unit coverage (round-trip, leakage check, tampering rejection, wrong-master-key rejection, IV non-determinism). - `KmsState` carries `master_key_bytes: Vec<u8>` (serde default generates a fresh random key on first construction; `reset()` preserves the key so existing ciphertexts still decrypt). - All `Encrypt`, `GenerateDataKey(WithoutPlaintext)?`, `GenerateDataKeyPair(WithoutPlaintext)?`, `ReEncrypt`, and the cross-service `KmsServiceHook::encrypt` paths now emit the new blob. - `Decrypt` and the hook's `decrypt` accept three formats: the new AWS-shaped blob, the legacy `fakecloud-kms:` text envelope, and the legacy `fakecloud-imported:` XOR envelope. Older ciphertexts persisted by prior versions still round-trip. E2E: new `kms_blob_format` suite asserts version header, plaintext non-leakage, GenerateDataKey wrapping, and legacy decrypt fallback. The cross-service KMS suites (sqs_kms, ssm_kms, secretsmanager_kms, sns_ddb_kms, ecr_kms_encryption, s3_sse_kms) and the KMS persistence test all stay green.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
1 issue found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fakecloud-kms/src/service.rs">
<violation number="1" location="crates/fakecloud-kms/src/service.rs:54">
P1: The blob header key ID is not cryptographically bound to the ciphertext, so `Decrypt` can return a tampered `KeyId` while still producing valid plaintext.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // Modern AWS-shaped blob (AES-256-GCM under the per-account master | ||
| // key) — try this first. Older textual envelopes fall through. | ||
| if let Some(decoded) = crate::blob::decode(&state.master_key_bytes, &ciphertext_bytes) { | ||
| let key = state.keys.get(&decoded.key_id).ok_or_else(|| { |
There was a problem hiding this comment.
P1: The blob header key ID is not cryptographically bound to the ciphertext, so Decrypt can return a tampered KeyId while still producing valid plaintext.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fakecloud-kms/src/service.rs, line 54:
<comment>The blob header key ID is not cryptographically bound to the ciphertext, so `Decrypt` can return a tampered `KeyId` while still producing valid plaintext.</comment>
<file context>
@@ -47,6 +47,26 @@ fn decode_ciphertext_envelope(
+ // Modern AWS-shaped blob (AES-256-GCM under the per-account master
+ // key) — try this first. Older textual envelopes fall through.
+ if let Some(decoded) = crate::blob::decode(&state.master_key_bytes, &ciphertext_bytes) {
+ let key = state.keys.get(&decoded.key_id).ok_or_else(|| {
+ AwsServiceError::aws_error(
+ StatusCode::BAD_REQUEST,
</file context>
Cubic flagged P1: the key-id in the blob header was a plain prefix. An attacker could rewrite the embedded KeyId without invalidating the AES-GCM ciphertext, so Decrypt would return tampered KeyId metadata alongside genuine plaintext. Pass the key-id as Associated Authenticated Data on encrypt and verify it on decrypt. Tampering with the header bytes flips the AAD, AES-GCM authentication fails, and decode returns None — the response falls through to InvalidCiphertextException.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the textual envelope (
fakecloud-kms:<key>:<base64-plaintext>) — which round-tripped through real SDKs but leaked plaintext to anyone who base64-decoded the bytes — with an AWS-compatible opaque binary ciphertext blob shaped as:Encrypted with AES-256-GCM under a per-account 32-byte master key generated lazily and persisted in
KmsState, so ciphertexts produced before a server restart still decrypt afterwards.fakecloud-kms::blob(encode/decode) with unit coverage: round-trip, leakage check, tampering rejection, wrong-master-key rejection, IV non-determinism.Encrypt,GenerateDataKey(WithoutPlaintext)?,GenerateDataKeyPair(WithoutPlaintext)?,ReEncrypt, and the cross-serviceKmsServiceHook::encryptpaths emit the new blob.Decryptand the hook'sdecryptaccept three formats: new blob, legacyfakecloud-kms:text envelope, legacyfakecloud-imported:XOR envelope. Older ciphertexts persisted by prior versions still round-trip.KmsState.master_key_bytesis#[serde(default = ...)]so old snapshots load with a fresh master key (their pre-existing ciphertexts use the legacy textual envelope, which still decrypts).Part of batch 9/10 of the gap-fill plan.
Test plan
cargo test -p fakecloud-kms --lib)kms_blob_format(3 tests): version header, plaintext non-leakage, GenerateDataKey wrapping, legacy decrypt fallbackcargo clippy --workspace --all-targets -- -D warningscleancargo fmt --allcleanSummary by cubic
Switch KMS ciphertext to an AWS-shaped opaque binary blob using AES-256-GCM, with the key-id bound as AEAD AAD to prevent plaintext leakage and header tampering. All encrypt paths now emit the blob; decrypt remains compatible with legacy formats.
New Features
fakecloud-kms::blob(encode/decode) for the AWS-shaped binary format; the key-id is bound via AEAD AAD so any header tampering fails decryption.KmsState.Encrypt,GenerateDataKey(WithoutPlaintext)?,GenerateDataKeyPair(WithoutPlaintext)?,ReEncrypt, andKmsServiceHook::encryptnow return the blob.Decrypt(and hook) accept: new blob, legacyfakecloud-kms:text, and legacyfakecloud-imported:XOR. New E2E tests cover version header, no plaintext leakage, wrapping behavior, and legacy fallback.Migration
Written for commit 015e561. Summary will update on new commits.