Skip to content

fix(sdk): allow the same KAS to wrap the same split (DSPX-3379)#967

Open
dmihalcik-virtru wants to merge 2 commits into
mainfrom
DSPX-3379-multisplit-allowed
Open

fix(sdk): allow the same KAS to wrap the same split (DSPX-3379)#967
dmihalcik-virtru wants to merge 2 commits into
mainfrom
DSPX-3379-multisplit-allowed

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Jul 9, 2026

Copy link
Copy Markdown
Member

What

When a TDF's key-access-object (KAO) array has two entries with the same split
id and the same KAS URI
, the web-sdk refused to decrypt, throwing
Unable to decrypt: Multiple keys detected for Key Access Server [...].

This treats the KAOs for a split as a disjunction of alternatives (any one
succeeding unwraps the split) instead of a URL-keyed map that can hold at most
one KAO per KAS — matching how the split is already consumed downstream
(anyPool) and how go-sdk / java-sdk already behave.

Changes (lib/tdf3/src/tdf.ts)

  • splitLookupTableFactory now returns Record<splitId, KeyAccessObject[]> and
    no longer throws on a repeated (splitId, kasUrl); every allowed KAO is kept
    as an alternative. Disallowed-KAS accounting (UnsafeUrlError) is unchanged.
  • unwrapKey feeds each alternative to anyPool with a unique url#kid#index
    key so duplicate KAS entries are each tried until one succeeds.

Why (DSPX-3379)

The same KAS wrapping the same split is legitimate — an authoring mistake (same
key used twice) or intentional multiple keys on one KAS. go/java read these
files fine; the web-sdk (e.g. secure viewer) failed. Expected: decrypt (or
permission-denied) exactly as if the copies lived on distinct URIs.

Tests

  • Unit: splitLookupTableFactory keeps duplicate (sid, kas) and
    same-KAS-different-KID entries as alternatives.
  • In-process decrypt path: a splitPlan with a repeated kas+sid produces a
    duplicate-KAO TDF that decrypts via the full unwrap/anyPool path.
  • Full mocha suite passes (334 passing).
  • Cross-SDK: test_tdf_with_duplicate_kao_same_kas (chore(xtest): cross-SDK test for duplicate KAO on same KAS+split (DSPX-3379) tests#555) — CI is
    green for go, java, and this branch's js.

Spec: spec/DSPX-3379.md.

Depends on / validated by: opentdf/tests#555

Summary by CodeRabbit

  • New Features

    • Improved decryption support for files that include multiple valid access entries for the same split, including repeated use of the same service.
    • Alternative access options are now handled more reliably during unlock attempts.
  • Bug Fixes

    • Fixed a decryption failure that occurred when duplicate access entries were present.
    • Files encrypted with repeated access settings can now be decrypted successfully.
  • Tests

    • Added and updated automated coverage for duplicate access-entry and decrypt scenarios.

@dmihalcik-virtru dmihalcik-virtru requested a review from a team as a code owner July 9, 2026 17:37
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dmihalcik-virtru, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12b08548-3dc0-4997-9a1b-dfcb027b18cc

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc6e33 and 8fc59c3.

📒 Files selected for processing (4)
  • lib/tdf3/src/tdf.ts
  • lib/tests/mocha/encrypt-decrypt.spec.ts
  • lib/tests/mocha/unit/tdf.spec.ts
  • spec/DSPX-3379.md
📝 Walkthrough

Walkthrough

Changes splitLookupTableFactory to return arrays of KeyAccessObjects per split id instead of a single-KAO map, removing the duplicate-URL error and preserving all alternatives. unwrapKey is updated to iterate these arrays with unique per-alternative keys. Tests and a spec document accompany the change.

Changes

Duplicate KAO support

Layer / File(s) Summary
Split table returns KAO arrays
lib/tdf3/src/tdf.ts
splitLookupTableFactory now returns Record<string, KeyAccessObject[]>, removing the InvalidFileError thrown on duplicate kao.url and instead appending all allowed KAOs per split id.
unwrapKey handles array-based potentials
lib/tdf3/src/tdf.ts
unwrapKey validates non-empty potentials arrays and builds unique rewrap keys via url#kid#index so duplicate KAO alternatives remain distinct during disjunctive rewrap attempts.
Unit and integration test coverage
lib/tests/mocha/unit/tdf.spec.ts, lib/tests/mocha/encrypt-decrypt.spec.ts
Unit tests updated to the array-based shape and now assert duplicate-URL and same-URL-different-kid entries are retained; a new integration test encrypts/decrypts a manifest containing two KAOs for the same split and KAS.
DSPX-3379 spec document
spec/DSPX-3379.md
New design document describing the failure mode, proposed contract change, edge cases, cross-SDK verification prompts, and acceptance criteria.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TDF as tdf.ts unwrapKey
  participant KAS

  Client->>TDF: unwrapKey(manifest)
  TDF->>TDF: splitLookupTableFactory builds splitId -> KAO[]
  loop for each split id
    TDF->>TDF: iterate potentials array, key by url#kid#index
    TDF->>KAS: rewrap request per alternative
    KAS-->>TDF: rewrap response
  end
  TDF-->>Client: unwrapped key
Loading

Possibly related PRs

  • opentdf/web-sdk#952: Both PRs touch lib/tdf3/src/tdf.ts's splitLookupTableFactory duplicate KeyAccessObject handling on the same code path, though this PR changes behavior to retain duplicates while the other only updates the error message.

Suggested reviewers: eugenioenko

Poem

A rabbit hops through KAS and split,
where duplicate keys once caused a rift.
Now arrays hold each wrapped alternative,
no error thrown, just options relative.
🐰 Decrypt away, the plaintext's safe and whole!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: allowing duplicate KAS entries for the same split during decryption.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3379-multisplit-allowed

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request resolves DSPX-3379 by allowing the same Key Access Server (KAS) to wrap the same split multiple times. It refactors splitLookupTableFactory to return a list of alternative Key Access Objects (KAOs) instead of throwing on duplicates, and updates unwrapKey to handle these duplicates as distinct alternatives. Feedback on the changes highlights a bug in splitLookupTableFactory where accessibleSplits does not normalize undefined sid values to empty strings, which can bypass safety checks. Additionally, a formatting improvement for a stack trace in the markdown specification file was suggested.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread lib/tdf3/src/tdf.ts
): Record<string, Record<string, KeyAccessObject>> {
): Record<string, KeyAccessObject[]> {
const allowed = (k: KeyAccessObject) => allowedKases.allows(k.url);
const splitIds = new Set(keyAccess.map(({ sid }) => sid ?? ''));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a subtle bug/inconsistency in how accessibleSplits (on line 713) is computed compared to splitIds (on line 711).

splitIds normalizes sid to '' if it is undefined:

const splitIds = new Set(keyAccess.map(({ sid }) => sid ?? ''));

However, accessibleSplits does not normalize sid:

const accessibleSplits = new Set(keyAccess.filter(allowed).map(({ sid }) => sid));

This means accessibleSplits can contain undefined instead of ''. If keyAccess contains both an allowed KAO with sid: undefined and an allowed KAO with sid: '', accessibleSplits will have size 2 (containing both undefined and ''), while splitIds will have size 1 (containing only '').

If there is also a disallowed split (e.g., sid: 'split1'), splitIds will have size 2 ('' and 'split1'), and accessibleSplits will also have size 2 (undefined and ''). This causes the check splitIds.size > accessibleSplits.size to evaluate to false (2 > 2), bypassing the early safety check in splitLookupTableFactory and throwing a less descriptive error later in unwrapKey.

To fix this, accessibleSplits should also normalize sid using sid ?? '':

const accessibleSplits = new Set(keyAccess.filter(allowed).map(({ sid }) => sid ?? ''));

Comment thread spec/DSPX-3379.md
We should allow the same KAS to wrap the same split. This could indicate a mistake during creation (the same key used multiple times) or an intended behavior (different keys on the same KAS indicating different security profiles of life cycles).
Behavior:
When a KAO array has two elements with the same split id and the same kas uri, it fails with the error:
TODO: Fallback to no split ids. Repetition found for [https://virtru.gbr.dev.internal] on split [71ce30e4-cd90-4269-adf7-c06db931338f]TdfError: TODO: Fallback to no split ids. Repetition found for [https://virtru.gbr.dev.internal] on split [71ce30e4-cd90-4269-adf7-c06db931338f] at splitLookupTableFactory (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29440:13) at unwrapKey (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29453:27) at decryptStreamFrom (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29691:75) at ZTDFReader.decrypt (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:30874:29)Expected behavior:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The stack trace on line 23 is currently formatted as a single long line, which makes it hard to read. It would be much cleaner to format it as a blockquote or a markdown code block.

For example:

Behavior:
When a KAO array has two elements with the same split id and the same kas uri, it fails with the error:

    TdfError: TODO: Fallback to no split ids. Repetition found for [https://virtru.gbr.dev.internal] on split [71ce30e4-cd90-4269-adf7-c06db931338f]
        at splitLookupTableFactory (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29440:13)
        at unwrapKey (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29453:27)
        at decryptStreamFrom (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:29691:75)
        at ZTDFReader.decrypt (https://virtru.gbr.dev.internal/secureviewer/assets/index-BySOd6k_.js:30874:29)

Expected behavior:

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3379-multisplit-allowed branch from 0cc6e33 to d370256 Compare July 10, 2026 14:54
@dmihalcik-virtru dmihalcik-virtru changed the title fix(tdf3): allow the same KAS to wrap the same split (DSPX-3379) fix(sdk): allow the same KAS to wrap the same split (DSPX-3379) Jul 10, 2026
multisplit allowed

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
splitLookupTableFactory now returns split id -> KeyAccessObject[] (a
disjunction of alternatives) instead of a url-keyed map, and no longer
throws when a KAS repeats within a split. unwrapKey feeds each alternative
to anyPool with a unique key so duplicate KAS entries are each tried until
one succeeds. Matches go-sdk/java-sdk behavior; validated by xtest
test_tdf_with_duplicate_kao_same_kas (opentdf/tests#555).

Adds unit tests for duplicate + same-KAS-different-KID, an in-process
decrypt-path test, and fills spec/DSPX-3379.md.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3379-multisplit-allowed branch from d370256 to 8fc59c3 Compare July 10, 2026 14:55
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

X-Test Failure Report

opentdf-ctl
opentdf-sdk-lib

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant