Repository navigation
fix(ruby): encrypt the record key to the owner public key on create - #1107
Merged
stas-schaller merged 1 commit intoAug 12, 2026
Merged
stas-schaller merged 1 commit into
stas-schaller merged 1 commit into
Conversation
prepare_create_payload assigned the raw AES-256 record key to payload.record_key with no encryption, while the same request carried the record data encrypted under that key. The server therefore received both a record's ciphertext and the key that opens it, which breaks the zero-knowledge property for every record the Ruby SDK creates. Present since 17.1.0, so 17.1.0 and 17.2.0 are both published with it. The fix reads appOwnerPublicKey from storage, fails if it is absent, and wraps the record key with Crypto.encrypt_ec, mirroring Python prepare_create_payload. Ruby's upload_file path already did this correctly, so only the create-record payload was affected. The hard failure is deliberate: a fallback to sending the key unwrapped would leave the hole open for exactly the older applications most likely to lack the key. Three regression tests assert the property on the payload bytes rather than on the code path. The second one is the defect stated as an assertion: it first confirms the record key really does decrypt payload.data, then requires that the value on the wire is not those bytes. The third forbids the silent-fallback fix. Before the fix all three fail, and the second reports expected and actual as byte-identical 32-byte strings. Five existing create_secret specs shared a config fixture with no owner public key, which is why the defect shipped twice with a green suite. The fixture now carries a real P-256 key. No assertion in those specs changed. Verified: 783 examples, 0 failures. Live check against QA created a record, confirmed payload.recordKey is 125 bytes beginning with 0x04 rather than the 32-byte plaintext key, read the record back with its field values intact, and deleted it.
stas-schaller
added a commit
that referenced
this pull request
Sep 15, 2026
* fix(ruby): encrypt the record key to the owner public key on create (#1107) prepare_create_payload assigned the raw AES-256 record key to payload.record_key with no encryption, while the same request carried the record data encrypted under that key. The server therefore received both a record's ciphertext and the key that opens it, which breaks the zero-knowledge property for every record the Ruby SDK creates. Present since 17.1.0, so 17.1.0 and 17.2.0 are both published with it. The fix reads appOwnerPublicKey from storage, fails if it is absent, and wraps the record key with Crypto.encrypt_ec, mirroring Python prepare_create_payload. Ruby's upload_file path already did this correctly, so only the create-record payload was affected. The hard failure is deliberate: a fallback to sending the key unwrapped would leave the hole open for exactly the older applications most likely to lack the key. Three regression tests assert the property on the payload bytes rather than on the code path. The second one is the defect stated as an assertion: it first confirms the record key really does decrypt payload.data, then requires that the value on the wire is not those bytes. The third forbids the silent-fallback fix. Before the fix all three fail, and the second reports expected and actual as byte-identical 32-byte strings. Five existing create_secret specs shared a config fixture with no owner public key, which is why the defect shipped twice with a green suite. The fixture now carries a real P-256 key. No assertion in those specs changed. Verified: 783 examples, 0 failures. Live check against QA created a record, confirmed payload.recordKey is 125 bytes beginning with 0x04 rather than the 32-byte plaintext key, read the record back with its field values intact, and deleted it. * docs(ruby): correct the affected version list for the record key fix The entry named 17.1.0 and 17.2.0. All four published versions carry the defect, including 17.0.3 and 17.0.4. Verified by downloading each gem from RubyGems and reading prepare_create_payload: the unwrapped assignment is present in every one, byte-identical. It is also present in the SDK's first commit, d026001, so it was never correct rather than introduced by a later change. The git history misled the original entry because the Ruby SDK's history in this repository begins at the v17.1.0 release, four months after 17.0.3 had already published. This matters to a reader rather than only to the record: naming two versions implicitly cleared the other two, so a customer on 17.0.4 would have taken a security note as an all-clear. * chore(ruby): set version to 17.2.1 for the record key security patch * docs(ruby): address review feedback on the 17.2.1 changelog and gemspec - move KSM-1193 CHANGELOG entry under ### Security (matches file's own precedent for security items) - clarify 17.0.0-17.0.2 were never published to RubyGems - scope "existing records" wording to records created by an earlier version - add missing [17.2.1] compare link - drop dates from all version headers in CHANGELOG.md/README.md - README: document create_secret's new raise and the re-bind/env-var remedy - gemspec: fix changelog_uri to point at sdk/ruby/CHANGELOG.md, not a nonexistent repo-root file
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
Ruby SDK:
create_secretsent the record key to the server unencrypted.prepare_create_payloadassigned the raw AES-256 record key topayload.record_key, while the same request carried the record data encrypted under that key. The server therefore received both a record's ciphertext and the key that opens it. This fix encrypts the record key to the application owner's public key first, matching the other SDKs.Changes
Fixed
prepare_create_payloadreadsappOwnerPublicKeyfrom storage and wraps the record key withCrypto.encrypt_ecbefore it goes on the wire (KSM-1193)prepare_create_payloadraises when the application configuration has no owner public key, instead of continuing without the encryption step (KSM-1193)The wrap mirrors Python
prepare_create_payload. Ruby'supload_filepath already encrypted its file record key correctly, so only the create-record payload was affected.The hard failure on a missing owner key is deliberate. A fallback to sending the key unwrapped would keep the defect for exactly the older applications most likely to lack that key.
Testing
783 examples, 0 failures.
Three new examples in
spec/keeper_secrets_manager/unit/create_secret_record_key_spec.rbassert the property on the payload bytes rather than on the code path:payload.record_keydecrypts back to the record key under the owner private key. A control assertion first proves the keypair and the two primitives round-trip, so a failure can only mean the wrap is absent.payload.data, and the value on the wire is not those bytes. The first half stops the second from passing when the fixture is unrelated.appOwnerPublicKeymakes the call raise rather than send the key unwrapped.All three fail on the unfixed code. The second reports expected and actual as byte-identical 32-byte strings.
Five existing
create_secretexamples shared a config fixture that had no owner public key, which is why this shipped twice with a green suite. The fixture now holds a real P-256 key. No assertion in those examples changed.Live check against QA created a record, confirmed
payload.recordKeyis 125 bytes beginning with0x04rather than the 32-byte plaintext key, read the record back with its title and field values intact, then deleted the record.Breaking Changes
None for callers whose application has an owner public key, which is every application bound by a current Web Vault or Commander. An application whose configuration lacks
appOwnerPublicKeynow receives an error fromcreate_secretinstead of creating a record. That path previously produced records with an unwrapped key, so it was never safe to use.Related Issues