feat(sdk): add TDF decrypt convenience helpers - #3828
Conversation
📝 WalkthroughWalkthroughThe SDK adds byte-, stream-, and file-based TDF decryption. It adds categorized errors, plaintext-size validation, temporary output handling, safe replacement, backup restoration, and cleanup reporting. ChangesSDK decryption
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant DecryptFile
participant LoadTDF
participant TemporaryOutput
participant FinalOutput
Caller->>DecryptFile: provide input and output paths
DecryptFile->>LoadTDF: load input TDF
LoadTDF->>TemporaryOutput: write decrypted content
DecryptFile->>FinalOutput: replace output after success
FinalOutput-->>DecryptFile: return finalization result
DecryptFile-->>Caller: return result or cleanup error
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@sdk/decrypt.go`:
- Around line 171-179: Update the finalization logic around the os.Rename calls
to always pass tmpPath through joinTempFileCleanup, including when restoring
backupPath fails, and include backupPath in the returned error so callers can
locate the original file. On the successful rename path, handle
os.Remove(backupPath) errors instead of discarding them, returning the cleanup
error according to the policy documented for joinTempFileCleanup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5c5cbb80-b86f-43e8-a13f-8d9eba84da5d
📒 Files selected for processing (3)
sdk/decrypt.gosdk/decrypt_test.gosdk/decrypterrors.go
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Two gaps in finalizeOutput's tail, flagged by CodeRabbit on #3828: - On a double rename failure (finalize fails, then backup-restore also fails), tmpPath was never cleaned up and the error never mentioned backupPath, leaving the caller with no way to find their original file. - On the success path, a failed os.Remove(backupPath) was silently discarded, contradicting this file's own documented policy of surfacing cleanup failures via joinTempFileCleanup rather than dropping them. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
DecryptBytes now rejects payloads over 1 GiB before buffering, since it holds the full plaintext in memory unlike DecryptTo/DecryptFile. Corresponds to opentdf/platform#3828. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Add DecryptBytes, DecryptTo, and DecryptFile on SDK — thin wrappers over LoadTDF + Reader.WriteTo for the common decrypt-to-plaintext case, so callers don't need to hand-roll the load/write-to sequence themselves. New sentinel errors ErrTDFNotDecryptable / ErrTDFDecryptFailed let callers branch on which stage failed via errors.Is, without losing the underlying LoadTDF/WriteTo error (reachable through the same chain via errors.As). DecryptFile writes to a temp file (mode 0600) in outputPath's directory and only renames onto outputPath once decryption fully succeeds. A pre-existing outputPath is moved aside to a reserved backup and restored if the final rename fails, so it's never left destroyed by a failed decrypt — only a successful finalize discards it. inputPath/outputPath referring to the same file (including via a hard link) is rejected up front via os.SameFile. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
The Quick Start's decrypt step hand-rolled LoadTDF + os.Create + io.Copy — exactly what DecryptTo replaces. Swap it in, and note DecryptBytes/ DecryptFile as the other two convenience shapes. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Two gaps in finalizeOutput's tail, flagged by CodeRabbit on #3828: - On a double rename failure (finalize fails, then backup-restore also fails), tmpPath was never cleaned up and the error never mentioned backupPath, leaving the caller with no way to find their original file. - On the success path, a failed os.Remove(backupPath) was silently discarded, contradicting this file's own documented policy of surfacing cleanup failures via joinTempFileCleanup rather than dropping them. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Matches the PR description's Before/after section — DecryptBytes was the only one with a full example; add DecryptTo and DecryptFile too. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
DecryptBytes buffers the full plaintext into memory, unlike DecryptTo/ DecryptFile which stream to their destination. Check the payload size (via Seek, from the manifest's already-known segment sizes) right after LoadTDF and before buffering anything, so an oversized payload fails fast with ErrTDFNotDecryptable instead of silently allocating a huge buffer. maxDecryptBytesSize is a var rather than a const so tests can override it without constructing a multi-gigabyte fixture. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
c78b5b9 to
f45d4a7
Compare
X-Test Failure Reportcukes-report |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
pflynn-virtru
left a comment
There was a problem hiding this comment.
No context.Context on any of the three new public methods, please add
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@sdk/decrypt.go`:
- Around line 112-120: Update the output-path validation in the decrypt flow
around the visible os.Stat checks and the later replacement logic: propagate any
input or output stat error unless it is explicitly os.ErrNotExist, while
preserving the same-file and directory checks for successful stats. Ensure the
replacement path only treats a missing output as absent, so permission, I/O, and
transient errors cannot lead to overwriting an existing file without backup.
In `@sdk/README.md`:
- Around line 107-119: Separate the Before and After snippets in the SDK README
into independent compilable code blocks, including the DecryptFile examples, so
declarations do not conflict across sections. Mark incomplete pseudocode as text
or provide all required setup and usage within each Go block, and remove or use
the unused in variable. Ensure the examples pass TestREADMECodeBlocks.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: fa347cec-bfc4-49f8-bdfa-5f50711d5e4b
📒 Files selected for processing (4)
sdk/README.mdsdk/decrypt.gosdk/decrypt_test.gosdk/decrypterrors.go
Per review feedback, all three new decrypt helpers now take ctx as their first parameter. LoadTDF/Reader.WriteTo don't accept a context, so ctx is threaded in via reader.Init(ctx) right after LoadTDF succeeds — Init does the KAS rewrap (the only network call these methods make) under ctx, and the subsequent WriteTo call skips its own key unwrap since the payload key is already set. Also fixes a stat-error handling bug flagged by CodeRabbit: DecryptFile and finalizeOutput treated any os.Stat error on outputPath as "doesn't exist," not just os.ErrNotExist. A permission or transient I/O error would silently bypass the same-file/directory checks and, in finalizeOutput, skip the backup-before-replace safety path entirely. Now only os.ErrNotExist is treated as absence; any other stat error is a hard failure. sdk/README.md: split the Before/After examples into separate fenced blocks (they previously shared one block with conflicting `:=` declarations across Before and After) and mark DecryptFile's elided Before pseudocode as `text` rather than `go`, since it deliberately leaves `in` unused. Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
|
🤖: @pflynn-virtru Done in b028c9d — all three now take |
…ss-link - All three Decrypt Helpers signatures/examples now take ctx context.Context as their first parameter, matching opentdf/platform#3828's review-driven signature change (ctx governs the KAS rewrap request). - Add <SdkVersion language="go" version="0.29.0" .../> under each of the three Signatures, matching the established per-Signature placement convention used elsewhere in this file. - Cross-link from assertion_examples.mdx's End-to-End Example to Decrypt Helpers, explaining why that example stays on LoadTDF directly (it reads tdfReader.Manifest().Assertions, which the helpers don't expose). Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
|
## Summary Documents the new Go SDK `DecryptBytes`/`DecryptTo`/`DecryptFile` convenience helpers added in opentdf/platform#3828. - Adds a **Decrypt Helpers** section to `docs/sdks/tdf.mdx` (between `LoadTDF` and `IsValidTdf`): signatures, examples, `DecryptFile`'s temp-file/rename/backup-restore behavior, and an Errors table covering `ErrTDFNotDecryptable`/`ErrTDFDecryptFailed` with an `errors.Is` example. - Updates `docs/sdks/quickstart/go.mdx` — all three places that manually did `LoadTDF` + `WriteTo` (the Step 3 running example, "Save TDF to a File", and the "Complete Reference Implementation") now use the new helpers. Modeled on the existing `BulkDecrypt` section's structure (Go-only convenience methods don't need forced `<Tabs>` shells for Java/JS). Heading names (`DecryptBytes`/`DecryptTo`/`DecryptFile`) are unique across the page and its imported `code_samples/*.mdx` files, per the anchor-collision rule in `AGENTS.md`. ## Test plan - [x] `vale docs/sdks/tdf.mdx docs/sdks/quickstart/go.mdx` — 0 errors, warnings, or suggestions. - [x] Every code example and behavior claim cross-checked against the actual implementation in opentdf/platform#3828 (`sdk/decrypt.go`, `sdk/decrypterrors.go`). - [x] Checked for heading/anchor collisions across `tdf.mdx` and its imports — none. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Simplified Go SDK quickstart examples for in-memory and file-based decryption. * Added guidance for decrypting data to memory, writers, and files. * Documented payload limits, streaming behavior, error classification, and file safety protections. * Clarified that manifest assertions are verified through the standard TDF loading flow. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Summary
Adds
DecryptBytes,DecryptTo, andDecryptFileonSDK— thin convenience wrappers overLoadTDF+Reader.WriteTofor the common decrypt-to-plaintext case.ctx context.Contextas their first parameter, governing the KAS rewrap request (the only network call each makes).LoadTDF/Reader.WriteTodon't accept a context, soctxis threaded in viareader.Init(ctx)right afterLoadTDFsucceeds —Initdoes the rewrap underctx, and the subsequentWriteTocall skips its own key unwrap since the payload key is already set.ErrTDFNotDecryptable/ErrTDFDecryptFailedlet callers branch on which stage failed viaerrors.Is.DecryptFilewrites to a temp file (mode0600, since it holds decrypted content) inoutputPath's directory and only renames ontooutputPathonce decryption fully succeeds.outputPathis moved aside to a reserved backup and restored if the final rename fails, so it's never left destroyed by a failed decrypt — only a successful finalize discards it.inputPath/outputPathreferring to the same file (including via a hard link) is rejected up front viaos.SameFile, since the final rename would otherwise replace the input while it may still be open.DecryptBytesrejects plaintext over 1 GiB up front, before buffering anything — it holds the full plaintext in memory, unlikeDecryptTo/DecryptFile, which stream to their destination.os.ErrNotExistis treated as "outputPath doesn't exist" (in both the up-front checks andfinalizeOutput) — any other stat error (permission, transient I/O) is a hard failure rather than silently bypassing the same-file/directory checks or the backup-before-replace safety path.Before / after
DecryptBytes— decrypt ciphertext bytes to plaintext bytes:DecryptTo— decrypt ciphertext bytes to anyio.Writer:DecryptFile— decrypt a TDF file to an output file:Errors
Every failure from all three helpers wraps one of these two sentinels — there's no third case. The underlying error (e.g. a KAS rewrap error, a payload integrity error, or the exact error a custom
TDFReaderOptionreturned) stays reachable viaerrors.Asthrough the same chain — nothing is swallowed or coerced away.Test plan
go test -race -count=1 ./sdk/...— all packages pass, including 15 new tests indecrypt_test.gocovering invalid ciphertext, missing input, unwritable output, output preserved/removed on load failure, same-path/hard-link rejection, directory-output rejection,TDFReaderOptionerror passthrough, the DecryptBytes size-limit rejection, andfinalizeOutput's replace-existing-output path, each assertingerrors.Isagainst the new sentinels where applicable — all offline, no live platform required.golangci-lint run ./sdk/...— 0 new issues (11 pre-existing issues elsewhere in the package are unrelated to this change).go vet ./sdk/...clean.