@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.
- package/dist/cjs/src/access/access-fetch.js +2 -1
- package/dist/cjs/src/access/access-rpc.js +11 -5
- package/dist/cjs/src/access/constants.js +6 -0
- package/dist/cjs/src/access.js +24 -4
- package/dist/cjs/src/nanoclients.js +4 -4
- package/dist/cjs/src/nanotdf/Client.js +10 -6
- package/dist/cjs/src/opentdf.js +39 -3
- package/dist/cjs/src/platform.js +5 -1
- package/dist/cjs/src/utils.js +29 -1
- package/dist/cjs/tdf3/src/client/DecoratedReadableStream.js +8 -1
- package/dist/cjs/tdf3/src/client/builders.js +1 -1
- package/dist/cjs/tdf3/src/client/index.js +14 -2
- package/dist/cjs/tdf3/src/tdf.js +23 -8
- package/dist/types/src/access/access-fetch.d.ts +1 -0
- package/dist/types/src/access/access-fetch.d.ts.map +1 -1
- package/dist/types/src/access/access-rpc.d.ts +2 -1
- package/dist/types/src/access/access-rpc.d.ts.map +1 -1
- package/dist/types/src/access/constants.d.ts +3 -0
- package/dist/types/src/access/constants.d.ts.map +1 -0
- package/dist/types/src/access.d.ts +15 -1
- package/dist/types/src/access.d.ts.map +1 -1
- package/dist/types/src/nanotdf/Client.d.ts +8 -1
- package/dist/types/src/nanotdf/Client.d.ts.map +1 -1
- package/dist/types/src/opentdf.d.ts +11 -0
- package/dist/types/src/opentdf.d.ts.map +1 -1
- package/dist/types/src/platform.d.ts +5 -0
- package/dist/types/src/platform.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +5 -0
- package/dist/types/src/utils.d.ts.map +1 -1
- package/dist/types/tdf3/src/client/DecoratedReadableStream.d.ts +6 -0
- package/dist/types/tdf3/src/client/DecoratedReadableStream.d.ts.map +1 -1
- package/dist/types/tdf3/src/client/builders.d.ts +2 -0
- package/dist/types/tdf3/src/client/builders.d.ts.map +1 -1
- package/dist/types/tdf3/src/client/index.d.ts +14 -1
- package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
- package/dist/types/tdf3/src/tdf.d.ts +1 -0
- package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
- package/dist/web/src/access/access-fetch.js +2 -1
- package/dist/web/src/access/access-rpc.js +11 -5
- package/dist/web/src/access/constants.js +3 -0
- package/dist/web/src/access.js +22 -3
- package/dist/web/src/nanoclients.js +4 -4
- package/dist/web/src/nanotdf/Client.js +11 -7
- package/dist/web/src/opentdf.js +39 -3
- package/dist/web/src/platform.js +5 -1
- package/dist/web/src/utils.js +28 -1
- package/dist/web/tdf3/src/client/DecoratedReadableStream.js +8 -1
- package/dist/web/tdf3/src/client/builders.js +1 -1
- package/dist/web/tdf3/src/client/index.js +14 -2
- package/dist/web/tdf3/src/tdf.js +23 -8
- package/package.json +1 -1
- package/src/access/access-fetch.ts +1 -0
- package/src/access/access-rpc.ts +13 -4
- package/src/access/constants.ts +2 -0
- package/src/access.ts +39 -2
- package/src/nanoclients.ts +3 -3
- package/src/nanotdf/Client.ts +28 -6
- package/src/opentdf.ts +58 -2
- package/src/platform.ts +10 -0
- package/src/utils.ts +32 -0
- package/tdf3/src/client/DecoratedReadableStream.ts +9 -0
- package/tdf3/src/client/builders.ts +1 -0
- package/tdf3/src/client/index.ts +30 -0
- package/tdf3/src/tdf.ts +27 -7
package/src/nanotdf/Client.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
package/tdf3/src/client/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
854
|
-
|
|
855
|
-
const
|
|
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:
|
|
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;
|