Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ export async function loadTDFStream(chunker: Chunker): Promise<InspectedTDFOverv
export function splitLookupTableFactory(
keyAccess: KeyAccessObject[],
allowedKases: OriginAllowList
): Record<string, Record<string, KeyAccessObject>> {
): Record<string, Record<string, KeyAccessObject[]>> {
const allowed = (k: KeyAccessObject) => allowedKases.allows(k.url);
const splitIds = new Set(keyAccess.map(({ sid }) => sid ?? ''));
Comment thread
dmihalcik-virtru marked this conversation as resolved.

Expand All @@ -720,18 +720,19 @@ export function splitLookupTableFactory(
...disallowedKases
);
}
const splitPotentials: Record<string, Record<string, KeyAccessObject>> = Object.fromEntries(
const splitPotentials: Record<string, Record<string, KeyAccessObject[]>> = Object.fromEntries(
[...splitIds].map((s) => [s, {}])
);
for (const kao of keyAccess) {
const disjunction = splitPotentials[kao.sid ?? ''];
if (kao.url in disjunction) {
throw new InvalidFileError(
`TODO: Fallback to no split ids. Repetition found for [${kao.url}] on split [${kao.sid}]`
);
if (!allowed(kao)) {
continue;
}
if (allowed(kao)) {
disjunction[kao.url] = kao;
const disjunction = splitPotentials[kao.sid ?? ''];
const existing = disjunction[kao.url];
if (existing) {
existing.push(kao);
} else {
disjunction[kao.url] = [kao];
}
}
return splitPotentials;
Expand Down Expand Up @@ -933,14 +934,17 @@ async function unwrapKey({
);
}
const anyPromises: Record<string, () => Promise<RewrapResponseData>> = {};
for (const [kas, keySplitInfo] of Object.entries(potentials)) {
anyPromises[kas] = async () => {
try {
return await tryKasRewrap(keySplitInfo);
} catch (e) {
throw handleRewrapError(e as Error);
}
};
for (const [kas, kaoList] of Object.entries(potentials)) {
kaoList.forEach((keySplitInfo, idx) => {
const key = kaoList.length === 1 ? kas : `${kas}#${idx}`;
anyPromises[key] = async () => {
try {
return await tryKasRewrap(keySplitInfo);
} catch (e) {
throw handleRewrapError(e as Error);
}
};
});
}
Comment thread
dmihalcik-virtru marked this conversation as resolved.
splitPromises[splitId] = () => anyPool(poolSize, anyPromises);
}
Expand Down
36 changes: 25 additions & 11 deletions lib/tests/mocha/unit/tdf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ describe('splitLookupTableFactory', () => {
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);

expect(result).to.deep.equal({
split1: { 'https://kas1': keyAccess[0] },
split2: { 'https://kas2': keyAccess[1] },
split1: { 'https://kas1': [keyAccess[0]] },
split2: { 'https://kas2': [keyAccess[1]] },
});
});

Expand All @@ -275,8 +275,8 @@ describe('splitLookupTableFactory', () => {
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);

expect(result).to.deep.equal({
split1: { 'https://kas1': keyAccess[0] },
split2: { 'https://kas2': keyAccess[1] },
split1: { 'https://kas1': [keyAccess[0]] },
split2: { 'https://kas2': [keyAccess[1]] },
});
});

Expand All @@ -293,17 +293,31 @@ describe('splitLookupTableFactory', () => {
);
});

it('should throw for duplicate URLs in the same splitId', () => {
it('preserves duplicate URLs in the same splitId as a list', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' }, // duplicate URL in same splitId
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
];
const allowedKases = new OriginAllowList(['https://kas1']);

expect(() => TDF.splitLookupTableFactory(keyAccess, allowedKases)).to.throw(
InvalidFileError,
'TODO: Fallback to no split ids. Repetition found for [https://kas1] on split [split1]'
);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);

expect(result).to.deep.equal({
split1: { 'https://kas1': [keyAccess[0], keyAccess[1]] },
});
});

it('preserves multiple keys with distinct kids on the same KAS and split', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'wrapped', url: 'https://kas1', kid: 'k1', protocol: 'kas' },
{ sid: 'split1', type: 'wrapped', url: 'https://kas1', kid: 'k2', protocol: 'kas' },
];
const allowedKases = new OriginAllowList(['https://kas1']);

const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);

expect(result['split1']['https://kas1']).to.have.length(2);
expect(result['split1']['https://kas1'].map((k) => k.kid)).to.deep.equal(['k1', 'k2']);
});

it('should handle empty keyAccess array', () => {
Expand Down Expand Up @@ -336,7 +350,7 @@ describe('splitLookupTableFactory', () => {
const result = TDF.splitLookupTableFactory(keyAccess, new OriginAllowList(allowedKases));

expect(result).to.deep.equal({
'': { 'https://kas1': keyAccess[0] },
'': { 'https://kas1': [keyAccess[0]] },
});
});
});
42 changes: 42 additions & 0 deletions spec/DSPX-3379.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
ticket: DSPX-3379
title: Web SDK fails if the same KAS gets more than one copy of the same split
status: draft
authors:
- dmihalcik@virtru.com
branches:
- opentdf/web-sdk:DSPX-3379-splitfix
prs: []
created: 2026-05-28T00:00:00Z
updated: 2026-05-28T00:00:00Z
jira_priority: Medium
---


# Web SDK fails if the same KAS gets more than one copy of the same split

## Summary
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:
The file should decrypt (or fail with permission denied) as it would if the keys were stored on distinct URIs

## Problem / Motivation
_Why does this work need to happen? What is the user/business pain?_

## Proposed Solution
_What will you build, at a functional level? Sketch the approach._

## Inputs / Outputs / Contracts
_Function signatures, data shapes, API contracts, CLI flags._

## Edge Cases & Constraints
_Boundary conditions, error states, performance limits, security considerations._

## Out of Scope
_What this work item explicitly does not cover._

## Acceptance Criteria
- [ ] _Clear, testable condition_
- [ ] _…_

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 specification file contains several empty sections with placeholder template text (e.g., Problem / Motivation, Proposed Solution, Inputs / Outputs / Contracts, Edge Cases & Constraints, Out of Scope, and Acceptance Criteria). Please fill out these sections to provide the necessary context and design details for this change, or remove them if they are not applicable.

Loading