@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.
@@ -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 { fetchWrappedKey, KasPublicKeyInfo, OriginAllowList } from '../access.js';
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: OriginAllowList;
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.allowedKases = new OriginAllowList(allowedKases || [kasEndpoint], !!ignoreAllowList);
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
- if (!this.allowedKases.allows(kasRewrapUrl)) {
218
- throw new UnsafeUrlError(`request URL ∉ ${this.allowedKases.origins};`, kasRewrapUrl);
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 { type KasPublicKeyAlgorithm, OriginAllowList, isPublicKeyAlgorithm } from './access.js';
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 = { ...this.defaultCreateOptions, ...opts };
337
- const collection = await this.createNanoTDFCollection(opts);
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
- const allowList = new OriginAllowList(
554
- this.opts.allowedKASEndpoints ?? [],
555
- this.opts.ignoreAllowlist
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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Exposes the released version number of the `@opentdf/sdk` package
3
3
  */
4
- export const version = '0.3.1';
4
+ export const version = '0.3.2';
5
5
 
6
6
  /**
7
7
  * A string name used to label requests as coming from this library client.
@@ -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?: string;
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 `[kasEndpoint]`.
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: OriginAllowList;
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 (!validateSecureUrl(this.kasEndpoint) && !this.allowedKases.allows(kasOrigin)) {
303
- throw new ConfigurationError(`Invalid KAS endpoint [${this.kasEndpoint}]`);
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}]; to force, please list it among allowedKases`
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