@opentdf/sdk 0.4.0-beta.40 → 0.4.0-beta.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cjs/src/access/access-fetch.js +2 -1
  2. package/dist/cjs/src/access/access-rpc.js +11 -5
  3. package/dist/cjs/src/access/constants.js +6 -0
  4. package/dist/cjs/src/access.js +24 -4
  5. package/dist/cjs/src/nanoclients.js +4 -4
  6. package/dist/cjs/src/nanotdf/Client.js +10 -6
  7. package/dist/cjs/src/opentdf.js +39 -3
  8. package/dist/cjs/src/platform.js +5 -1
  9. package/dist/cjs/src/utils.js +29 -1
  10. package/dist/cjs/tdf3/src/client/DecoratedReadableStream.js +8 -1
  11. package/dist/cjs/tdf3/src/client/builders.js +1 -1
  12. package/dist/cjs/tdf3/src/client/index.js +14 -2
  13. package/dist/cjs/tdf3/src/tdf.js +23 -8
  14. package/dist/types/src/access/access-fetch.d.ts +1 -0
  15. package/dist/types/src/access/access-fetch.d.ts.map +1 -1
  16. package/dist/types/src/access/access-rpc.d.ts +2 -1
  17. package/dist/types/src/access/access-rpc.d.ts.map +1 -1
  18. package/dist/types/src/access/constants.d.ts +3 -0
  19. package/dist/types/src/access/constants.d.ts.map +1 -0
  20. package/dist/types/src/access.d.ts +15 -1
  21. package/dist/types/src/access.d.ts.map +1 -1
  22. package/dist/types/src/nanotdf/Client.d.ts +8 -1
  23. package/dist/types/src/nanotdf/Client.d.ts.map +1 -1
  24. package/dist/types/src/opentdf.d.ts +11 -0
  25. package/dist/types/src/opentdf.d.ts.map +1 -1
  26. package/dist/types/src/platform.d.ts +5 -0
  27. package/dist/types/src/platform.d.ts.map +1 -1
  28. package/dist/types/src/utils.d.ts +5 -0
  29. package/dist/types/src/utils.d.ts.map +1 -1
  30. package/dist/types/tdf3/src/client/DecoratedReadableStream.d.ts +6 -0
  31. package/dist/types/tdf3/src/client/DecoratedReadableStream.d.ts.map +1 -1
  32. package/dist/types/tdf3/src/client/builders.d.ts +2 -0
  33. package/dist/types/tdf3/src/client/builders.d.ts.map +1 -1
  34. package/dist/types/tdf3/src/client/index.d.ts +14 -1
  35. package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
  36. package/dist/types/tdf3/src/tdf.d.ts +1 -0
  37. package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
  38. package/dist/web/src/access/access-fetch.js +2 -1
  39. package/dist/web/src/access/access-rpc.js +11 -5
  40. package/dist/web/src/access/constants.js +3 -0
  41. package/dist/web/src/access.js +22 -3
  42. package/dist/web/src/nanoclients.js +4 -4
  43. package/dist/web/src/nanotdf/Client.js +11 -7
  44. package/dist/web/src/opentdf.js +39 -3
  45. package/dist/web/src/platform.js +5 -1
  46. package/dist/web/src/utils.js +28 -1
  47. package/dist/web/tdf3/src/client/DecoratedReadableStream.js +8 -1
  48. package/dist/web/tdf3/src/client/builders.js +1 -1
  49. package/dist/web/tdf3/src/client/index.js +14 -2
  50. package/dist/web/tdf3/src/tdf.js +23 -8
  51. package/package.json +1 -1
  52. package/src/access/access-fetch.ts +1 -0
  53. package/src/access/access-rpc.ts +13 -4
  54. package/src/access/constants.ts +2 -0
  55. package/src/access.ts +39 -2
  56. package/src/nanoclients.ts +3 -3
  57. package/src/nanotdf/Client.ts +28 -6
  58. package/src/opentdf.ts +58 -2
  59. package/src/platform.ts +10 -0
  60. package/src/utils.ts +32 -0
  61. package/tdf3/src/client/DecoratedReadableStream.ts +9 -0
  62. package/tdf3/src/client/builders.ts +1 -0
  63. package/tdf3/src/client/index.ts +30 -0
  64. package/tdf3/src/tdf.ts +27 -7
@@ -10,10 +10,16 @@ import {
10
10
  } from '../access.js';
11
11
  import { AuthProvider, isAuthProvider, reqSignature } from '../auth/providers.js';
12
12
  import { ConfigurationError, DecryptError, TdfError, UnsafeUrlError } from '../errors.js';
13
- import { cryptoPublicToPem, pemToCryptoPublicKey, validateSecureUrl } from '../utils.js';
13
+ import {
14
+ cryptoPublicToPem,
15
+ getRequiredObligationFQNs,
16
+ pemToCryptoPublicKey,
17
+ validateSecureUrl,
18
+ } from '../utils.js';
14
19
 
15
20
  export interface ClientConfig {
16
21
  allowedKases?: string[];
22
+ fulfillableObligationFQNs?: string[];
17
23
  ignoreAllowList?: boolean;
18
24
  authProvider: AuthProvider;
19
25
  dpopEnabled?: boolean;
@@ -23,6 +29,11 @@ export interface ClientConfig {
23
29
  platformUrl: string;
24
30
  }
25
31
 
32
+ type RewrapKeyResult = {
33
+ unwrappedKey: CryptoKey;
34
+ requiredObligations: string[];
35
+ };
36
+
26
37
  function toJWSAlg(c: CryptoKey): string {
27
38
  const { algorithm } = c;
28
39
  switch (algorithm.name) {
@@ -106,6 +117,7 @@ export default class Client {
106
117
  static readonly IV_SIZE = 12;
107
118
 
108
119
  allowedKases?: OriginAllowList;
120
+ readonly fulfillableObligationFQNs: string[];
109
121
  /*
110
122
  These variables are expected to be either assigned during initialization or within the methods.
111
123
  This is needed as the flow is very specific. Errors should be thrown if the necessary step is not completed.
@@ -168,6 +180,7 @@ export default class Client {
168
180
  } else {
169
181
  const {
170
182
  allowedKases,
183
+ fulfillableObligationFQNs = [],
171
184
  ignoreAllowList,
172
185
  authProvider,
173
186
  dpopEnabled,
@@ -184,6 +197,7 @@ export default class Client {
184
197
  if (allowedKases?.length || ignoreAllowList) {
185
198
  this.allowedKases = new OriginAllowList(allowedKases || [], ignoreAllowList);
186
199
  }
200
+ this.fulfillableObligationFQNs = fulfillableObligationFQNs;
187
201
  this.dpopEnabled = !!dpopEnabled;
188
202
  if (dpopKeys) {
189
203
  this.requestSignerKeyPair = dpopKeys;
@@ -223,7 +237,7 @@ export default class Client {
223
237
  kasRewrapUrl: string,
224
238
  magicNumberVersion: ArrayBufferLike,
225
239
  clientVersion: string
226
- ): Promise<CryptoKey> {
240
+ ): Promise<RewrapKeyResult> {
227
241
  let allowedKases = this.allowedKases;
228
242
 
229
243
  if (!allowedKases) {
@@ -265,10 +279,15 @@ export default class Client {
265
279
  });
266
280
 
267
281
  // Wrapped
268
- const wrappedKey = await fetchWrappedKey(kasRewrapUrl, signedRequestToken, this.authProvider);
282
+ const rewrapResp = await fetchWrappedKey(
283
+ kasRewrapUrl,
284
+ signedRequestToken,
285
+ this.authProvider,
286
+ this.fulfillableObligationFQNs
287
+ );
269
288
 
270
289
  // Extract the iv and ciphertext
271
- const entityWrappedKey = wrappedKey.entityWrappedKey;
290
+ const entityWrappedKey = rewrapResp.entityWrappedKey;
272
291
  const ivLength =
273
292
  clientVersion == Client.SDK_INITIAL_RELEASE ? Client.INITIAL_RELEASE_IV_SIZE : Client.IV_SIZE;
274
293
  const iv = entityWrappedKey.subarray(0, ivLength);
@@ -277,7 +296,7 @@ export default class Client {
277
296
  let kasPublicKey;
278
297
  try {
279
298
  // Let us import public key as a cert or public key
280
- kasPublicKey = await pemToCryptoPublicKey(wrappedKey.sessionPublicKey);
299
+ kasPublicKey = await pemToCryptoPublicKey(rewrapResp.sessionPublicKey);
281
300
  } catch (cause) {
282
301
  throw new ConfigurationError(
283
302
  `internal: [${kasRewrapUrl}] PEM Public Key to crypto public key failed. Is PEM formatted correctly?`,
@@ -346,6 +365,9 @@ export default class Client {
346
365
  throw new DecryptError('Unable to import raw key.', cause);
347
366
  }
348
367
 
349
- return unwrappedKey;
368
+ return {
369
+ requiredObligations: getRequiredObligationFQNs(rewrapResp),
370
+ unwrappedKey: unwrappedKey,
371
+ };
350
372
  }
351
373
  }
package/src/opentdf.ts CHANGED
@@ -55,6 +55,12 @@ export type Keys = {
55
55
  [keyID: string]: CryptoKey | CryptoKeyPair;
56
56
  };
57
57
 
58
+ /** The fully qualified obligations that the caller is required to fulfill. */
59
+ export type RequiredObligations = {
60
+ /** List of obligations values' fully qualified names. */
61
+ fqns: string[];
62
+ };
63
+
58
64
  /** Options for creating a new TDF object, shared between all container types. */
59
65
  export type CreateOptions = {
60
66
  /** If the policy service should be used to control creation options. */
@@ -156,6 +162,8 @@ export type ReadOptions = {
156
162
  allowedKASEndpoints?: string[];
157
163
  /** Optionally disable checking the allowlist. */
158
164
  ignoreAllowlist?: boolean;
165
+ /** Optionally override client fulfillableObligationFQNs. */
166
+ fulfillableObligationFQNs?: string[];
159
167
  /** Public (or shared) keys for verifying assertions. */
160
168
  assertionVerificationKeys?: AssertionVerificationKeys;
161
169
  /** Optionally disable assertion verification. */
@@ -307,6 +315,11 @@ export type TDFReader = {
307
315
  * @returns Any data attributes found in the policy. Currently only works for plain text, embedded policies (not remote or encrypted policies)
308
316
  */
309
317
  attributes: () => Promise<string[]>;
318
+
319
+ /**
320
+ * @returns Any obligation value FQNs that are required to be fulfilled on the TDF, populated during the decrypt flow.
321
+ */
322
+ obligations: () => Promise<RequiredObligations>;
310
323
  };
311
324
 
312
325
  /**
@@ -543,11 +556,18 @@ class UnknownTypeReader {
543
556
  this.state = 'done';
544
557
  });
545
558
  }
559
+
560
+ async obligations() {
561
+ const actual = await this.delegate;
562
+ return actual.obligations();
563
+ }
546
564
  }
547
565
 
548
566
  /** A TDF reader for NanoTDF files. */
549
567
  class NanoTDFReader {
550
568
  container: Promise<NanoTDF>;
569
+ // Required obligation FQNs that must be fulfilled, provided via the decrypt flow.
570
+ private requiredObligations?: RequiredObligations;
551
571
  constructor(
552
572
  readonly outer: OpenTDF,
553
573
  readonly opts: ReadOptions,
@@ -573,7 +593,10 @@ class NanoTDFReader {
573
593
  });
574
594
  }
575
595
 
576
- /** Decrypts the NanoTDF file and returns a decorated stream. */
596
+ /**
597
+ * Decrypts the NanoTDF file and returns a decorated stream.
598
+ * Sets required obligations on the reader when retrieved from KAS rewrap response.
599
+ */
577
600
  async decrypt(): Promise<DecoratedStream> {
578
601
  const nanotdf = await this.container;
579
602
  const cachedDEK = this.rewrapCache.get(nanotdf.header.ephemeralPublicKey);
@@ -587,6 +610,7 @@ class NanoTDFReader {
587
610
  this.opts.allowedKASEndpoints?.[0] || platformUrl || 'https://disallow.all.invalid';
588
611
  const nc = new Client({
589
612
  allowedKases: this.opts.allowedKASEndpoints,
613
+ fulfillableObligationFQNs: this.opts.fulfillableObligationFQNs,
590
614
  authProvider: this.outer.authProvider,
591
615
  ignoreAllowList: this.opts.ignoreAllowlist,
592
616
  dpopEnabled: this.outer.dpopEnabled,
@@ -597,7 +621,7 @@ class NanoTDFReader {
597
621
  // TODO: The version number should be fetched from the API
598
622
  const version = '0.0.1';
599
623
  // Rewrap key on every request
600
- const dek = await nc.rewrapKey(
624
+ const { unwrappedKey: dek, requiredObligations } = await nc.rewrapKey(
601
625
  nanotdf.header.toBuffer(),
602
626
  nanotdf.header.getKasRewrapUrl(),
603
627
  nanotdf.header.magicNumberVersion,
@@ -607,6 +631,7 @@ class NanoTDFReader {
607
631
  // These should have thrown already.
608
632
  throw new Error('internal: key rewrap failure');
609
633
  }
634
+ this.requiredObligations = { fqns: requiredObligations };
610
635
  this.rewrapCache.set(nanotdf.header.ephemeralPublicKey, dek);
611
636
  const r: DecoratedStream = await streamify(decryptNanoTDF(dek, nanotdf));
612
637
  // TODO figure out how to attach policy and metadata to the stream
@@ -634,11 +659,25 @@ class NanoTDFReader {
634
659
  const policy = JSON.parse(policyString) as Policy;
635
660
  return policy?.body?.dataAttributes.map((a) => a.attribute) || [];
636
661
  }
662
+
663
+ /**
664
+ * Returns obligations populated from the decrypt flow.
665
+ * If a decrypt has not occurred, attempts one to retrieve obligations.
666
+ */
667
+ async obligations(): Promise<RequiredObligations> {
668
+ if (this.requiredObligations) {
669
+ return this.requiredObligations;
670
+ }
671
+ await this.decrypt();
672
+ return this.requiredObligations ?? { fqns: [] };
673
+ }
637
674
  }
638
675
 
639
676
  /** A reader for TDF files. */
640
677
  class ZTDFReader {
641
678
  overview: Promise<InspectedTDFOverview>;
679
+ // Required obligation FQNs that must be fulfilled, provided via the decrypt flow.
680
+ private requiredObligations?: RequiredObligations;
642
681
  constructor(
643
682
  readonly client: TDF3Client,
644
683
  readonly opts: ReadOptions,
@@ -650,6 +689,7 @@ class ZTDFReader {
650
689
  /**
651
690
  * Decrypts the TDF file and returns a decorated stream.
652
691
  * The stream will have a manifest and metadata attached if available.
692
+ * Sets required obligations on the reader when retrieved from KAS rewrap response.
653
693
  */
654
694
  async decrypt(): Promise<DecoratedStream> {
655
695
  const {
@@ -695,9 +735,13 @@ class ZTDFReader {
695
735
  assertionVerificationKeys,
696
736
  noVerifyAssertions,
697
737
  wrappingKeyAlgorithm,
738
+ fulfillableObligations: this.opts.fulfillableObligationFQNs || [],
698
739
  },
699
740
  overview
700
741
  );
742
+ this.requiredObligations = {
743
+ fqns: oldStream.obligations(),
744
+ };
701
745
  const stream: DecoratedStream = oldStream.stream;
702
746
  stream.manifest = Promise.resolve(overview.manifest);
703
747
  stream.metadata = Promise.resolve(oldStream.metadata);
@@ -721,6 +765,18 @@ class ZTDFReader {
721
765
  const policy = JSON.parse(policyJSON) as Policy;
722
766
  return policy?.body?.dataAttributes.map((a) => a.attribute) || [];
723
767
  }
768
+
769
+ /**
770
+ * Returns obligations populated from the decrypt flow.
771
+ * If a decrypt has not occurred, attempts one to retrieve obligations.
772
+ */
773
+ async obligations(): Promise<RequiredObligations> {
774
+ if (this.requiredObligations) {
775
+ return this.requiredObligations;
776
+ }
777
+ await this.decrypt();
778
+ return this.requiredObligations ?? { fqns: [] };
779
+ }
724
780
  }
725
781
 
726
782
  async function streamify(ab: Promise<ArrayBuffer>): Promise<ReadableStream<Uint8Array>> {
package/src/platform.ts CHANGED
@@ -8,6 +8,7 @@ import { AuthProvider } from '../tdf3/index.js';
8
8
  import { Client, createClient, Interceptor } from '@connectrpc/connect';
9
9
  import { WellKnownService } from './platform/wellknownconfiguration/wellknown_configuration_pb.js';
10
10
  import { AuthorizationService } from './platform/authorization/authorization_pb.js';
11
+ import { AuthorizationService as AuthorizationServiceV2 } from './platform/authorization/v2/authorization_pb.js';
11
12
  import { EntityResolutionService } from './platform/entityresolution/entity_resolution_pb.js';
12
13
  import { AccessService } from './platform/kas/kas_pb.js';
13
14
  import { ActionService } from './platform/policy/actions/actions_pb.js';
@@ -32,6 +33,10 @@ export interface PlatformServices {
32
33
  wellknown: Client<typeof WellKnownService>;
33
34
  }
34
35
 
36
+ export interface PlatformServicesV2 {
37
+ authorization: Client<typeof AuthorizationServiceV2>;
38
+ }
39
+
35
40
  export interface PlatformClientOptions {
36
41
  /** Optional authentication provider for generating auth interceptor. */
37
42
  authProvider?: AuthProvider;
@@ -68,6 +73,7 @@ export interface PlatformClientOptions {
68
73
 
69
74
  export class PlatformClient {
70
75
  readonly v1: PlatformServices;
76
+ readonly v2: PlatformServicesV2;
71
77
 
72
78
  constructor(options: PlatformClientOptions) {
73
79
  const interceptors: Interceptor[] = [];
@@ -99,6 +105,10 @@ export class PlatformClient {
99
105
  unsafe: createClient(UnsafeService, transport),
100
106
  wellknown: createClient(WellKnownService, transport),
101
107
  };
108
+
109
+ this.v2 = {
110
+ authorization: createClient(AuthorizationServiceV2, transport),
111
+ };
102
112
  }
103
113
  }
104
114
 
package/src/utils.ts CHANGED
@@ -3,8 +3,11 @@ import { exportSPKI, importX509 } from 'jose';
3
3
  import { base64 } from './encodings/index.js';
4
4
  import { pemCertToCrypto, pemPublicToCrypto } from './nanotdf-crypto/pemPublicToCrypto.js';
5
5
  import { ConfigurationError } from './errors.js';
6
+ import { RewrapResponse } from './platform/kas/kas_pb.js';
6
7
  import { ConnectError } from '@connectrpc/connect';
7
8
 
9
+ const REQUIRED_OBLIGATIONS_METADATA_KEY = 'X-Required-Obligations';
10
+
8
11
  /**
9
12
  * Check to see if the given URL is 'secure'. This assumes:
10
13
  *
@@ -223,3 +226,32 @@ export function getPlatformUrlFromKasEndpoint(endpoint: string): string {
223
226
  }
224
227
  return result;
225
228
  }
229
+
230
+ /**
231
+ * Retrieves the fully qualified Obligations (values) that must be fulfilled from a rewrap response.
232
+ */
233
+ export function getRequiredObligationFQNs(response: RewrapResponse) {
234
+ const requiredObligations = new Set<string>();
235
+
236
+ // Loop through response key access object results, checking proto values/types for a metadata key
237
+ // that matches the expected KAS-provided fulfillable obligations list.
238
+ for (const resp of response.responses) {
239
+ for (const result of resp.results) {
240
+ if (!result.metadata.hasOwnProperty(REQUIRED_OBLIGATIONS_METADATA_KEY)) {
241
+ continue;
242
+ }
243
+ const value = result.metadata[REQUIRED_OBLIGATIONS_METADATA_KEY];
244
+ if (value?.kind.case !== 'listValue') {
245
+ continue;
246
+ }
247
+ const obligations = value.kind.value.values;
248
+ for (const obligation of obligations) {
249
+ if (obligation.kind.case === 'stringValue') {
250
+ requiredObligations.add(obligation.kind.value.toLowerCase());
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ return [...requiredObligations.values()];
257
+ }
@@ -21,6 +21,7 @@ export class DecoratedReadableStream {
21
21
  metadata?: Metadata;
22
22
  manifest: Manifest;
23
23
  fileStreamServiceWorker?: string;
24
+ requiredObligations?: string[];
24
25
 
25
26
  constructor(
26
27
  underlyingSource: UnderlyingSource & {
@@ -60,6 +61,14 @@ export class DecoratedReadableStream {
60
61
  async toString(): Promise<string> {
61
62
  return new Response(this.stream).text();
62
63
  }
64
+
65
+ /**
66
+ * The fully qualified obligations required to be fulfilled on stream contents
67
+ * are set as decoration during the decrypt flow.
68
+ */
69
+ obligations(): string[] {
70
+ return this.requiredObligations ?? [];
71
+ }
63
72
  }
64
73
 
65
74
  export function isDecoratedReadableStream(s: unknown): s is DecoratedReadableStream {
@@ -538,6 +538,7 @@ export type DecryptParams = {
538
538
  concurrencyLimit?: number;
539
539
  noVerifyAssertions?: boolean;
540
540
  wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
541
+ fulfillableObligationFQNs?: string[];
541
542
  };
542
543
 
543
544
  /**
@@ -143,6 +143,12 @@ export interface ClientConfig {
143
143
  * Defaults to `[]`.
144
144
  */
145
145
  allowedKases?: string[];
146
+ /**
147
+ * List of obligation value FQNs in platform policy that can be fulfilled
148
+ * by the PEP handling this client (i.e. 'https://example.com/obl/drm/value/mask').
149
+ * Defaults to '[]'.
150
+ */
151
+ fulfillableObligationFQNs?: string[];
146
152
  // Platform URL to use to lookup allowed KASes when allowedKases is empty
147
153
  platformUrl?: string;
148
154
  ignoreAllowList?: boolean;
@@ -337,6 +343,13 @@ export class Client {
337
343
  */
338
344
  readonly allowedKases?: OriginAllowList;
339
345
 
346
+ /**
347
+ * List of obligation value FQNs in platform policy that can be fulfilled
348
+ * by the PEP utilizing this client (i.e. 'https://example.com/obl/drm/value/mask').
349
+ * Defaults to '[]'. Currently set per Client and not per TDF.
350
+ */
351
+ readonly fulfillableObligationFQNs: string[];
352
+
340
353
  /**
341
354
  * URL of the platform, required to fetch list of allowed KASes when allowedKases is empty
342
355
  */
@@ -417,6 +430,14 @@ export class Client {
417
430
  }
418
431
  }
419
432
 
433
+ this.fulfillableObligationFQNs = config.fulfillableObligationFQNs?.length
434
+ ? config.fulfillableObligationFQNs
435
+ : [];
436
+
437
+ if (clientConfig.easEndpoint) {
438
+ this.easEndpoint = clientConfig.easEndpoint;
439
+ }
440
+
420
441
  this.authProvider = config.authProvider;
421
442
  this.clientConfig = clientConfig;
422
443
 
@@ -736,6 +757,7 @@ export class Client {
736
757
  * @param params.source A data stream object, one of remote, stream, buffer, etc. types.
737
758
  * @param params.eo Optional entity object (legacy AuthZ)
738
759
  * @param params.assertionVerificationKeys Optional verification keys for assertions.
760
+ * @param params.fulfillableObligationFQNs Optional fulfillable obligation value FQNs (overrides those on the Client)
739
761
  * @return a {@link https://nodejs.org/api/stream.html#stream_class_stream_readable|Readable} stream containing the decrypted plaintext.
740
762
  * @see DecryptParamsBuilder
741
763
  */
@@ -748,6 +770,7 @@ export class Client {
748
770
  noVerifyAssertions,
749
771
  concurrencyLimit = 1,
750
772
  wrappingKeyAlgorithm,
773
+ fulfillableObligationFQNs = [],
751
774
  }: DecryptParams): Promise<DecoratedReadableStream> {
752
775
  const dpopKeys = await this.dpopKeys;
753
776
  if (!this.authProvider) {
@@ -762,6 +785,12 @@ export class Client {
762
785
  throw new ConfigurationError('platformUrl is required when allowedKases is empty');
763
786
  }
764
787
 
788
+ const hasEmptyDecryptParamObligationsButGlobal =
789
+ !fulfillableObligationFQNs.length && this.fulfillableObligationFQNs.length;
790
+ if (hasEmptyDecryptParamObligationsButGlobal) {
791
+ fulfillableObligationFQNs = this.fulfillableObligationFQNs;
792
+ }
793
+
765
794
  // Await in order to catch any errors from this call.
766
795
  // TODO: Write error event to stream and don't await.
767
796
  return await (streamMiddleware as DecryptStreamMiddleware)(
@@ -778,6 +807,7 @@ export class Client {
778
807
  assertionVerificationKeys,
779
808
  noVerifyAssertions,
780
809
  wrappingKeyAlgorithm,
810
+ fulfillableObligations: fulfillableObligationFQNs,
781
811
  })
782
812
  );
783
813
  }
package/tdf3/src/tdf.ts CHANGED
@@ -55,6 +55,7 @@ import { ZipReader, ZipWriter, keyMerge, concatUint8, buffToString } from './uti
55
55
  import { CentralDirectory } from './utils/zip-reader.js';
56
56
  import { ztdfSalt } from './crypto/salt.js';
57
57
  import { Payload } from './models/payload.js';
58
+ import { getRequiredObligationFQNs } from '../../src/utils.js';
58
59
 
59
60
  // TODO: input validation on manifest JSON
60
61
  const DEFAULT_SEGMENT_SIZE = 1024 * 1024;
@@ -152,6 +153,7 @@ export type EncryptConfiguration = {
152
153
  };
153
154
 
154
155
  export type DecryptConfiguration = {
156
+ fulfillableObligations: string[];
155
157
  allowedKases?: string[];
156
158
  allowList?: OriginAllowList;
157
159
  authProvider: AuthProvider;
@@ -735,6 +737,7 @@ export function splitLookupTableFactory(
735
737
  type RewrapResponseData = {
736
738
  key: Uint8Array;
737
739
  metadata: Record<string, unknown>;
740
+ requiredObligations: string[];
738
741
  };
739
742
 
740
743
  async function unwrapKey({
@@ -745,6 +748,7 @@ async function unwrapKey({
745
748
  concurrencyLimit,
746
749
  cryptoService,
747
750
  wrappingKeyAlgorithm,
751
+ fulfillableObligations,
748
752
  }: {
749
753
  manifest: Manifest;
750
754
  allowedKases: OriginAllowList;
@@ -753,6 +757,7 @@ async function unwrapKey({
753
757
  dpopKeys: CryptoKeyPair;
754
758
  cryptoService: CryptoService;
755
759
  wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
760
+ fulfillableObligations: string[];
756
761
  }) {
757
762
  if (authProvider === undefined) {
758
763
  throw new ConfigurationError(
@@ -788,11 +793,14 @@ async function unwrapKey({
788
793
  const jwtPayload = { requestBody: requestBodyStr };
789
794
  const signedRequestToken = await reqSignature(jwtPayload, dpopKeys.privateKey);
790
795
 
791
- const { entityWrappedKey, metadata, sessionPublicKey } = await fetchWrappedKey(
796
+ const rewrapResp = await fetchWrappedKey(
792
797
  url,
793
798
  signedRequestToken,
794
- authProvider
799
+ authProvider,
800
+ fulfillableObligations
795
801
  );
802
+ const { entityWrappedKey, metadata, sessionPublicKey } = rewrapResp;
803
+ const requiredObligations = getRequiredObligationFQNs(rewrapResp);
796
804
 
797
805
  if (wrappingKeyAlgorithm === 'ec:secp256r1') {
798
806
  const serverEphemeralKey: CryptoKey = await pemPublicToCrypto(sessionPublicKey);
@@ -810,6 +818,7 @@ async function unwrapKey({
810
818
  return {
811
819
  key: new Uint8Array(dek),
812
820
  metadata,
821
+ requiredObligations,
813
822
  };
814
823
  }
815
824
  const key = Binary.fromArrayBuffer(entityWrappedKey);
@@ -821,6 +830,7 @@ async function unwrapKey({
821
830
  return {
822
831
  key: new Uint8Array(decryptedKeyBinary.asByteArray()),
823
832
  metadata,
833
+ requiredObligations,
824
834
  };
825
835
  }
826
836
 
@@ -850,12 +860,20 @@ async function unwrapKey({
850
860
  splitPromises[splitId] = () => anyPool(poolSize, anyPromises);
851
861
  }
852
862
  try {
853
- const splitResults = await allPool(poolSize, splitPromises);
854
- // Merge all the split keys
855
- const reconstructedKey = keyMerge(splitResults.map((r) => r.key));
863
+ const rewrapResponseData = await allPool(poolSize, splitPromises);
864
+ const splitKeys = [];
865
+ const requiredObligations = new Set<string>();
866
+ for (const resp of rewrapResponseData) {
867
+ splitKeys.push(resp.key);
868
+ for (const requiredObligation of resp.requiredObligations) {
869
+ requiredObligations.add(requiredObligation.toLowerCase());
870
+ }
871
+ }
872
+ const reconstructedKey = keyMerge(splitKeys);
856
873
  return {
857
874
  reconstructedKeyBinary: Binary.fromArrayBuffer(reconstructedKey),
858
- metadata: splitResults[0].metadata, // Use metadata from first split
875
+ metadata: rewrapResponseData[0].metadata, // Use metadata from first split
876
+ requiredObligations: [...requiredObligations],
859
877
  };
860
878
  } catch (e) {
861
879
  if (e instanceof AggregateError) {
@@ -1039,7 +1057,8 @@ export async function decryptStreamFrom(
1039
1057
  segmentSizeDefault,
1040
1058
  segments,
1041
1059
  } = manifest.encryptionInformation.integrityInformation;
1042
- const { metadata, reconstructedKeyBinary } = await unwrapKey({
1060
+ const { metadata, reconstructedKeyBinary, requiredObligations } = await unwrapKey({
1061
+ fulfillableObligations: cfg.fulfillableObligations,
1043
1062
  manifest,
1044
1063
  authProvider: cfg.authProvider,
1045
1064
  allowedKases: allowList,
@@ -1162,6 +1181,7 @@ export async function decryptStreamFrom(
1162
1181
 
1163
1182
  const outputStream = new DecoratedReadableStream(underlyingSource);
1164
1183
 
1184
+ outputStream.requiredObligations = requiredObligations;
1165
1185
  outputStream.manifest = manifest;
1166
1186
  outputStream.metadata = metadata;
1167
1187
  return outputStream;