@opentdf/sdk 0.3.1 → 0.3.2-beta.2277
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.js +46 -1
- package/dist/cjs/src/nanotdf/Client.js +12 -6
- package/dist/cjs/src/opentdf.js +36 -5
- package/dist/cjs/src/version.js +1 -1
- package/dist/cjs/tdf3/src/client/index.js +18 -10
- package/dist/cjs/tdf3/src/tdf.js +8 -1
- package/dist/types/src/access.d.ts +1 -0
- package/dist/types/src/access.d.ts.map +1 -1
- package/dist/types/src/nanotdf/Client.d.ts +3 -1
- package/dist/types/src/nanotdf/Client.d.ts.map +1 -1
- package/dist/types/src/opentdf.d.ts +5 -1
- package/dist/types/src/opentdf.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/tdf3/src/client/index.d.ts +8 -3
- package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
- package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
- package/dist/web/src/access.js +45 -1
- package/dist/web/src/nanotdf/Client.js +13 -7
- package/dist/web/src/opentdf.js +37 -6
- package/dist/web/src/version.js +1 -1
- package/dist/web/tdf3/src/client/index.js +19 -11
- package/dist/web/tdf3/src/tdf.js +8 -1
- package/package.json +1 -1
- package/src/access.ts +48 -0
- package/src/nanotdf/Client.ts +22 -6
- package/src/opentdf.ts +57 -7
- package/src/version.ts +1 -1
- package/tdf3/src/client/index.ts +28 -11
- package/tdf3/src/tdf.ts +7 -0
package/src/nanotdf/Client.ts
CHANGED
|
@@ -2,7 +2,12 @@ import * as base64 from '../encodings/base64.js';
|
|
|
2
2
|
import { generateKeyPair, keyAgreement } from '../nanotdf-crypto/index.js';
|
|
3
3
|
import getHkdfSalt from './helpers/getHkdfSalt.js';
|
|
4
4
|
import DefaultParams from './models/DefaultParams.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fetchKeyAccessServers,
|
|
7
|
+
fetchWrappedKey,
|
|
8
|
+
KasPublicKeyInfo,
|
|
9
|
+
OriginAllowList,
|
|
10
|
+
} from '../access.js';
|
|
6
11
|
import { AuthProvider, isAuthProvider, reqSignature } from '../auth/providers.js';
|
|
7
12
|
import { ConfigurationError, DecryptError, TdfError, UnsafeUrlError } from '../errors.js';
|
|
8
13
|
import { cryptoPublicToPem, pemToCryptoPublicKey, validateSecureUrl } from '../utils.js';
|
|
@@ -15,6 +20,7 @@ export interface ClientConfig {
|
|
|
15
20
|
dpopKeys?: Promise<CryptoKeyPair>;
|
|
16
21
|
ephemeralKeyPair?: Promise<CryptoKeyPair>;
|
|
17
22
|
kasEndpoint: string;
|
|
23
|
+
platformUrl: string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function toJWSAlg(c: CryptoKey): string {
|
|
@@ -99,12 +105,13 @@ export default class Client {
|
|
|
99
105
|
static readonly INITIAL_RELEASE_IV_SIZE = 3;
|
|
100
106
|
static readonly IV_SIZE = 12;
|
|
101
107
|
|
|
102
|
-
allowedKases
|
|
108
|
+
allowedKases?: OriginAllowList;
|
|
103
109
|
/*
|
|
104
110
|
These variables are expected to be either assigned during initialization or within the methods.
|
|
105
111
|
This is needed as the flow is very specific. Errors should be thrown if the necessary step is not completed.
|
|
106
112
|
*/
|
|
107
113
|
protected kasUrl: string;
|
|
114
|
+
readonly platformUrl: string;
|
|
108
115
|
kasPubKey?: KasPublicKeyInfo;
|
|
109
116
|
readonly authProvider: AuthProvider;
|
|
110
117
|
readonly dpopEnabled: boolean;
|
|
@@ -150,7 +157,6 @@ export default class Client {
|
|
|
150
157
|
// TODO Disallow http KAS. For now just log as error
|
|
151
158
|
validateSecureUrl(kasUrl);
|
|
152
159
|
this.kasUrl = kasUrl;
|
|
153
|
-
this.allowedKases = new OriginAllowList([kasUrl]);
|
|
154
160
|
this.dpopEnabled = dpopEnabled;
|
|
155
161
|
|
|
156
162
|
if (ephemeralKeyPair) {
|
|
@@ -168,12 +174,16 @@ export default class Client {
|
|
|
168
174
|
dpopKeys,
|
|
169
175
|
ephemeralKeyPair,
|
|
170
176
|
kasEndpoint,
|
|
177
|
+
platformUrl,
|
|
171
178
|
} = optsOrOldAuthProvider;
|
|
172
179
|
this.authProvider = enwrapAuthProvider(authProvider);
|
|
173
180
|
// TODO Disallow http KAS. For now just log as error
|
|
174
181
|
validateSecureUrl(kasEndpoint);
|
|
175
182
|
this.kasUrl = kasEndpoint;
|
|
176
|
-
this.
|
|
183
|
+
this.platformUrl = platformUrl;
|
|
184
|
+
if (allowedKases?.length || ignoreAllowList) {
|
|
185
|
+
this.allowedKases = new OriginAllowList(allowedKases || [], ignoreAllowList);
|
|
186
|
+
}
|
|
177
187
|
this.dpopEnabled = !!dpopEnabled;
|
|
178
188
|
if (dpopKeys) {
|
|
179
189
|
this.requestSignerKeyPair = dpopKeys;
|
|
@@ -214,8 +224,14 @@ export default class Client {
|
|
|
214
224
|
magicNumberVersion: ArrayBufferLike,
|
|
215
225
|
clientVersion: string
|
|
216
226
|
): Promise<CryptoKey> {
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
let allowedKases = this.allowedKases;
|
|
228
|
+
|
|
229
|
+
if (!allowedKases) {
|
|
230
|
+
allowedKases = await fetchKeyAccessServers(this.platformUrl, this.authProvider);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!allowedKases.allows(kasRewrapUrl)) {
|
|
234
|
+
throw new UnsafeUrlError(`request URL ∉ ${allowedKases.origins};`, kasRewrapUrl);
|
|
219
235
|
}
|
|
220
236
|
|
|
221
237
|
const ephemeralKeyPair = await this.ephemeralKeyPair;
|
package/src/opentdf.ts
CHANGED
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
AssertionConfig,
|
|
14
14
|
AssertionVerificationKeys,
|
|
15
15
|
} from '../tdf3/src/assertions.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type KasPublicKeyAlgorithm,
|
|
18
|
+
OriginAllowList,
|
|
19
|
+
fetchKeyAccessServers,
|
|
20
|
+
isPublicKeyAlgorithm,
|
|
21
|
+
} from './access.js';
|
|
17
22
|
import { type Manifest } from '../tdf3/src/models/manifest.js';
|
|
18
23
|
import { type Payload } from '../tdf3/src/models/payload.js';
|
|
19
24
|
import {
|
|
@@ -87,6 +92,7 @@ export type CreateNanoTDFOptions = CreateOptions & {
|
|
|
87
92
|
};
|
|
88
93
|
|
|
89
94
|
export type CreateNanoTDFCollectionOptions = CreateNanoTDFOptions & {
|
|
95
|
+
platformUrl: string;
|
|
90
96
|
// The maximum number of key iterations to use for a single DEK.
|
|
91
97
|
maxKeyIterations?: number;
|
|
92
98
|
};
|
|
@@ -136,6 +142,8 @@ export type CreateZTDFOptions = CreateOptions & {
|
|
|
136
142
|
export type ReadOptions = {
|
|
137
143
|
// ciphertext
|
|
138
144
|
source: Source;
|
|
145
|
+
// Platform URL
|
|
146
|
+
platformUrl?: string;
|
|
139
147
|
// list of KASes that may be contacted for a rewrap
|
|
140
148
|
allowedKASEndpoints?: string[];
|
|
141
149
|
// Optionally disable checking the allowlist
|
|
@@ -157,6 +165,9 @@ export type OpenTDFOptions = {
|
|
|
157
165
|
// Policy service endpoint
|
|
158
166
|
policyEndpoint?: string;
|
|
159
167
|
|
|
168
|
+
// Platform URL
|
|
169
|
+
platformUrl?: string;
|
|
170
|
+
|
|
160
171
|
// Auth provider for connections to the policy service and KASes.
|
|
161
172
|
authProvider: AuthProvider;
|
|
162
173
|
|
|
@@ -286,6 +297,7 @@ export type TDFReader = {
|
|
|
286
297
|
// SDK for dealing with OpenTDF data and policy services.
|
|
287
298
|
export class OpenTDF {
|
|
288
299
|
// Configuration service and more is at this URL/connectRPC endpoint
|
|
300
|
+
readonly platformUrl: string;
|
|
289
301
|
readonly policyEndpoint: string;
|
|
290
302
|
readonly authProvider: AuthProvider;
|
|
291
303
|
readonly dpopEnabled: boolean;
|
|
@@ -305,11 +317,19 @@ export class OpenTDF {
|
|
|
305
317
|
disableDPoP,
|
|
306
318
|
policyEndpoint,
|
|
307
319
|
rewrapCacheOptions,
|
|
320
|
+
platformUrl,
|
|
308
321
|
}: OpenTDFOptions) {
|
|
309
322
|
this.authProvider = authProvider;
|
|
310
323
|
this.defaultCreateOptions = defaultCreateOptions || {};
|
|
311
324
|
this.defaultReadOptions = defaultReadOptions || {};
|
|
312
325
|
this.dpopEnabled = !!disableDPoP;
|
|
326
|
+
if (platformUrl) {
|
|
327
|
+
this.platformUrl = platformUrl;
|
|
328
|
+
} else {
|
|
329
|
+
console.warn(
|
|
330
|
+
"Warning: 'platformUrl' is required for security to ensure the SDK uses the platform-configured Key Access Server list"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
313
333
|
this.policyEndpoint = policyEndpoint || '';
|
|
314
334
|
this.rewrapCache = new RewrapCache(rewrapCacheOptions);
|
|
315
335
|
this.tdf3Client = new TDF3Client({
|
|
@@ -333,8 +353,14 @@ export class OpenTDF {
|
|
|
333
353
|
}
|
|
334
354
|
|
|
335
355
|
async createNanoTDF(opts: CreateNanoTDFOptions): Promise<DecoratedStream> {
|
|
336
|
-
opts = {
|
|
337
|
-
|
|
356
|
+
opts = {
|
|
357
|
+
...this.defaultCreateOptions,
|
|
358
|
+
...opts,
|
|
359
|
+
};
|
|
360
|
+
const collection = await this.createNanoTDFCollection({
|
|
361
|
+
...opts,
|
|
362
|
+
platformUrl: this.platformUrl,
|
|
363
|
+
});
|
|
338
364
|
try {
|
|
339
365
|
return await collection.encrypt(opts.source);
|
|
340
366
|
} finally {
|
|
@@ -415,6 +441,9 @@ class UnknownTypeReader {
|
|
|
415
441
|
this.state = 'resolving';
|
|
416
442
|
const chunker = await fromSource(this.opts.source);
|
|
417
443
|
const prefix = await chunker(0, 3);
|
|
444
|
+
if (!this.opts.platformUrl && this.outer.platformUrl) {
|
|
445
|
+
this.opts.platformUrl = this.outer.platformUrl;
|
|
446
|
+
}
|
|
418
447
|
if (prefix[0] === 0x50 && prefix[1] === 0x4b) {
|
|
419
448
|
this.state = 'loaded';
|
|
420
449
|
return new ZTDFReader(this.outer.tdf3Client, this.opts, chunker);
|
|
@@ -466,6 +495,13 @@ class NanoTDFReader {
|
|
|
466
495
|
readonly chunker: Chunker,
|
|
467
496
|
private readonly rewrapCache: RewrapCache
|
|
468
497
|
) {
|
|
498
|
+
if (
|
|
499
|
+
!this.opts.ignoreAllowlist &&
|
|
500
|
+
!this.outer.platformUrl &&
|
|
501
|
+
!this.opts.allowedKASEndpoints?.length
|
|
502
|
+
) {
|
|
503
|
+
throw new ConfigurationError('platformUrl is required when allowedKasEndpoints is empty');
|
|
504
|
+
}
|
|
469
505
|
// lazily load the container
|
|
470
506
|
this.container = new Promise(async (resolve, reject) => {
|
|
471
507
|
try {
|
|
@@ -493,6 +529,7 @@ class NanoTDFReader {
|
|
|
493
529
|
dpopEnabled: this.outer.dpopEnabled,
|
|
494
530
|
dpopKeys: this.outer.dpopKeys,
|
|
495
531
|
kasEndpoint: this.opts.allowedKASEndpoints?.[0] || 'https://disallow.all.invalid',
|
|
532
|
+
platformUrl: this.outer.platformUrl,
|
|
496
533
|
});
|
|
497
534
|
// TODO: The version number should be fetched from the API
|
|
498
535
|
const version = '0.0.1';
|
|
@@ -550,10 +587,11 @@ class ZTDFReader {
|
|
|
550
587
|
noVerify: noVerifyAssertions,
|
|
551
588
|
wrappingKeyAlgorithm,
|
|
552
589
|
} = this.opts;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
590
|
+
|
|
591
|
+
if (!this.opts.ignoreAllowlist && !this.opts.allowedKASEndpoints && !this.opts.platformUrl) {
|
|
592
|
+
throw new ConfigurationError('platformUrl is required when allowedKasEndpoints is empty');
|
|
593
|
+
}
|
|
594
|
+
|
|
557
595
|
const dpopKeys = await this.client.dpopKeys;
|
|
558
596
|
|
|
559
597
|
const { authProvider, cryptoService } = this.client;
|
|
@@ -561,6 +599,17 @@ class ZTDFReader {
|
|
|
561
599
|
throw new ConfigurationError('authProvider is required');
|
|
562
600
|
}
|
|
563
601
|
|
|
602
|
+
let allowList: OriginAllowList | undefined;
|
|
603
|
+
|
|
604
|
+
if (this.opts.allowedKASEndpoints?.length || this.opts.ignoreAllowlist) {
|
|
605
|
+
allowList = new OriginAllowList(
|
|
606
|
+
this.opts.allowedKASEndpoints || [],
|
|
607
|
+
this.opts.ignoreAllowlist
|
|
608
|
+
);
|
|
609
|
+
} else if (this.opts.platformUrl) {
|
|
610
|
+
allowList = await fetchKeyAccessServers(this.opts.platformUrl, authProvider);
|
|
611
|
+
}
|
|
612
|
+
|
|
564
613
|
const overview = await this.overview;
|
|
565
614
|
const oldStream = await decryptStreamFrom(
|
|
566
615
|
{
|
|
@@ -646,6 +695,7 @@ class Collection {
|
|
|
646
695
|
authProvider,
|
|
647
696
|
kasEndpoint: opts.defaultKASEndpoint ?? 'https://disallow.all.invalid',
|
|
648
697
|
maxKeyIterations: opts.maxKeyIterations,
|
|
698
|
+
platformUrl: opts.platformUrl,
|
|
649
699
|
});
|
|
650
700
|
}
|
|
651
701
|
|
package/src/version.ts
CHANGED
package/tdf3/src/client/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
EncryptParamsBuilder,
|
|
40
40
|
} from './builders.js';
|
|
41
41
|
import {
|
|
42
|
+
fetchKeyAccessServers,
|
|
42
43
|
type KasPublicKeyInfo,
|
|
43
44
|
keyAlgorithmToPublicKeyAlgorithm,
|
|
44
45
|
OriginAllowList,
|
|
@@ -125,7 +126,7 @@ export interface ClientConfig {
|
|
|
125
126
|
clientId?: string;
|
|
126
127
|
dpopEnabled?: boolean;
|
|
127
128
|
dpopKeys?: Promise<CryptoKeyPair>;
|
|
128
|
-
kasEndpoint
|
|
129
|
+
kasEndpoint: string;
|
|
129
130
|
/**
|
|
130
131
|
* Service to use to look up ABAC. Used during autoconfigure. Defaults to
|
|
131
132
|
* kasEndpoint without the trailing `/kas` path segment, if present.
|
|
@@ -133,9 +134,11 @@ export interface ClientConfig {
|
|
|
133
134
|
policyEndpoint?: string;
|
|
134
135
|
/**
|
|
135
136
|
* List of allowed KASes to connect to for rewrap requests.
|
|
136
|
-
* Defaults to `[
|
|
137
|
+
* Defaults to `[]`.
|
|
137
138
|
*/
|
|
138
139
|
allowedKases?: string[];
|
|
140
|
+
// Platform URL to use to lookup allowed KASes when allowedKases is empty
|
|
141
|
+
platformUrl?: string;
|
|
139
142
|
ignoreAllowList?: boolean;
|
|
140
143
|
easEndpoint?: string;
|
|
141
144
|
// DEPRECATED Ignored
|
|
@@ -237,7 +240,12 @@ export class Client {
|
|
|
237
240
|
* List of allowed KASes to connect to for rewrap requests.
|
|
238
241
|
* Defaults to `[this.kasEndpoint]`.
|
|
239
242
|
*/
|
|
240
|
-
readonly allowedKases
|
|
243
|
+
readonly allowedKases?: OriginAllowList;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* URL of the platform, required to fetch list of allowed KASes when allowedKases is empty
|
|
247
|
+
*/
|
|
248
|
+
readonly platformUrl?: string;
|
|
241
249
|
|
|
242
250
|
readonly kasKeys: Record<string, Promise<KasPublicKeyInfo>[]> = {};
|
|
243
251
|
|
|
@@ -287,6 +295,14 @@ export class Client {
|
|
|
287
295
|
this.kasEndpoint = clientConfig.keyRewrapEndpoint.replace(/\/rewrap$/, '');
|
|
288
296
|
}
|
|
289
297
|
this.kasEndpoint = rstrip(this.kasEndpoint, '/');
|
|
298
|
+
|
|
299
|
+
if (!validateSecureUrl(this.kasEndpoint)) {
|
|
300
|
+
throw new ConfigurationError(`Invalid KAS endpoint [${this.kasEndpoint}]`);
|
|
301
|
+
}
|
|
302
|
+
if (config.platformUrl) {
|
|
303
|
+
this.platformUrl = config.platformUrl;
|
|
304
|
+
}
|
|
305
|
+
|
|
290
306
|
if (clientConfig.policyEndpoint) {
|
|
291
307
|
this.policyEndpoint = rstrip(clientConfig.policyEndpoint, '/');
|
|
292
308
|
} else if (this.kasEndpoint.endsWith('/kas')) {
|
|
@@ -299,16 +315,12 @@ export class Client {
|
|
|
299
315
|
clientConfig.allowedKases,
|
|
300
316
|
!!clientConfig.ignoreAllowList
|
|
301
317
|
);
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
if (!validateSecureUrl(this.kasEndpoint)) {
|
|
318
|
+
if (!this.allowedKases.allows(kasOrigin)) {
|
|
319
|
+
// TODO PR: ask if in this cases it makes more sense to add defaultKASEndpoint to the allow list if the allowList is not empty but doesn't have the defaultKas
|
|
307
320
|
throw new ConfigurationError(
|
|
308
|
-
`Invalid KAS endpoint [${this.kasEndpoint}]
|
|
321
|
+
`Invalid KAS endpoint [${this.kasEndpoint}]. When allowedKases is set, defaultKASEndpoint needs to be in the allow list`
|
|
309
322
|
);
|
|
310
323
|
}
|
|
311
|
-
this.allowedKases = new OriginAllowList([kasOrigin], !!clientConfig.ignoreAllowList);
|
|
312
324
|
}
|
|
313
325
|
|
|
314
326
|
this.authProvider = config.authProvider;
|
|
@@ -445,6 +457,7 @@ export class Client {
|
|
|
445
457
|
? maxByteLimit
|
|
446
458
|
: opts.byteLimit;
|
|
447
459
|
const encryptionInformation = new SplitKey(new AesGcmCipher(this.cryptoService));
|
|
460
|
+
// TODO KAS: check here
|
|
448
461
|
const splits: SplitStep[] = splitPlan?.length
|
|
449
462
|
? splitPlan
|
|
450
463
|
: [{ kas: opts.defaultKASEndpoint ?? this.kasEndpoint }];
|
|
@@ -531,8 +544,12 @@ export class Client {
|
|
|
531
544
|
throw new ConfigurationError('AuthProvider missing');
|
|
532
545
|
}
|
|
533
546
|
const chunker = await makeChunkable(source);
|
|
534
|
-
if (!allowList) {
|
|
547
|
+
if (!allowList && this.allowedKases) {
|
|
535
548
|
allowList = this.allowedKases;
|
|
549
|
+
} else if (this.platformUrl) {
|
|
550
|
+
allowList = await fetchKeyAccessServers(this.platformUrl, this.authProvider);
|
|
551
|
+
} else {
|
|
552
|
+
throw new ConfigurationError('platformUrl is required when allowedKases is empty');
|
|
536
553
|
}
|
|
537
554
|
|
|
538
555
|
// Await in order to catch any errors from this call.
|
package/tdf3/src/tdf.ts
CHANGED
|
@@ -991,6 +991,13 @@ export async function readStream(cfg: DecryptConfiguration) {
|
|
|
991
991
|
return decryptStreamFrom(cfg, overview);
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
// TODO: potentially might need fixing here
|
|
995
|
+
// By the time this function is called the allow list will be already set.
|
|
996
|
+
// Verify that this function is not exported in the sdk and only exported for internal use
|
|
997
|
+
// Verify this during tests and PR
|
|
998
|
+
// Remove this comment before merging!
|
|
999
|
+
// https://www.youtube.com/watch?v=NGrLb6W5YOM
|
|
1000
|
+
// Don't leave me here all by myself!
|
|
994
1001
|
export async function decryptStreamFrom(
|
|
995
1002
|
cfg: DecryptConfiguration,
|
|
996
1003
|
{ manifest, zipReader, centralDirectory }: InspectedTDFOverview
|