feat(envelope): AES-256-GCM seam for user-secret values (RIG-3655 T1) - #1111
Merged
trunk-io[bot] merged 1 commit intoSep 12, 2026
Conversation
rigel-mintaka
added this pull request to stack #1112
September 11, 2026 20:05
|
Compass engineering docs preview: https://compass-server-rig-3655-t1-e.compass-eng-docs.pages.dev Deployed from |
This was referenced Sep 11, 2026
T1 of the user-secret store: the crypto primitive the Postgres-backed store encrypts values with. Pure package, no Postgres, no callers yet -- T2 adds the columns that use it. The API makes the two classic AEAD misuses unrepresentable rather than documented. `Encrypt` takes no nonce parameter and draws a fresh 96-bit nonce from crypto/rand per call, so nonce reuse has no expression. The key bytes live in an unexported [32]byte, so no reflection-based logger or marshaler can reach them, and `NewKey` copies its input so the caller can zero its own slice. `Decrypt` collapses every failure -- wrong key, tampered ciphertext, tampered nonce, wrong AAD, wrong nonce length -- to one opaque `ErrDecrypt` that wraps no cipher internals. A caller cannot learn which part it got wrong. `UserSecretAAD` binds the full scope tuple from the A9 amendment: version tag, tenant, scope kind, scope id, name, key version, NUL-separated so the encoding is injective. Binding the value to its row identity is what makes a row that is copied to another tenant, renamed, or re-scoped fail to decrypt instead of silently resolving. Tests are red-green and each defends a property rather than a line. I mutation-checked the suite: removing the NUL separators, fixing the nonce, and dropping the scope field from the AAD each turn it red, so the injectivity, freshness, and scope-binding assertions can all actually fail. stdlib crypto only -- no golang.org/x/crypto. Refs RIG-3655 Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
force-pushed
the
compass-server/rig-3655-t1-envelope
branch
from
September 12, 2026 02:50
b2b75fb to
e9e174d
Compare
mattwilkinsonn
approved these changes
Sep 12, 2026
|
This pull request was merged into |
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.
This PR is part of a stack containing 7 PRs:
mainT1 of the user-secret store: the crypto primitive the Postgres-backed store
encrypts values with. Pure package, no Postgres, no callers yet -- T2 adds the
columns that use it.
The API makes the two classic AEAD misuses unrepresentable rather than
documented.
Encrypttakes no nonce parameter and draws a fresh 96-bit noncefrom crypto/rand per call, so nonce reuse has no expression. The key bytes live
in an unexported [32]byte, so no reflection-based logger or marshaler can reach
them, and
NewKeycopies its input so the caller can zero its own slice.Decryptcollapses every failure -- wrong key, tampered ciphertext, tamperednonce, wrong AAD, wrong nonce length -- to one opaque
ErrDecryptthat wraps nocipher internals. A caller cannot learn which part it got wrong.
UserSecretAADbinds the full scope tuple from the A9 amendment: version tag,tenant, scope kind, scope id, name, key version, NUL-separated so the encoding
is injective. Binding the value to its row identity is what makes a row that is
copied to another tenant, renamed, or re-scoped fail to decrypt instead of
silently resolving.
Tests are red-green and each defends a property rather than a line. I
mutation-checked the suite: removing the NUL separators, fixing the nonce, and
dropping the scope field from the AAD each turn it red, so the injectivity,
freshness, and scope-binding assertions can all actually fail.
stdlib crypto only -- no golang.org/x/crypto.
Refs RIG-3655
Co-authored-by: Matt Wilkinson matt@rigel.build