@opentdf/sdk 0.5.0-beta.47 → 0.5.0-beta.49

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.
@@ -1,4 +1,8 @@
1
- import * as base64 from '../encodings/base64.js';
1
+ import { create, toJsonString } from '@bufbuild/protobuf';
2
+ import {
3
+ UnsignedRewrapRequest_WithPolicyRequestSchema,
4
+ UnsignedRewrapRequestSchema,
5
+ } from '../platform/kas/kas_pb.js';
2
6
  import { generateKeyPair, keyAgreement } from '../nanotdf-crypto/index.js';
3
7
  import getHkdfSalt from './helpers/getHkdfSalt.js';
4
8
  import DefaultParams from './models/DefaultParams.js';
@@ -8,13 +12,16 @@ import {
8
12
  KasPublicKeyInfo,
9
13
  OriginAllowList,
10
14
  } from '../access.js';
15
+ import { handleRpcRewrapErrorString } from '../../src/access/access-rpc.js';
11
16
  import { AuthProvider, isAuthProvider, reqSignature } from '../auth/providers.js';
12
17
  import { ConfigurationError, DecryptError, TdfError, UnsafeUrlError } from '../errors.js';
13
18
  import {
14
19
  cryptoPublicToPem,
15
20
  getRequiredObligationFQNs,
16
21
  pemToCryptoPublicKey,
22
+ upgradeRewrapResponseV1,
17
23
  validateSecureUrl,
24
+ getPlatformUrlFromKasEndpoint,
18
25
  } from '../utils.js';
19
26
 
20
27
  export interface ClientConfig {
@@ -260,18 +267,35 @@ export default class Client {
260
267
  throw new ConfigurationError('Signer key has not been set or generated');
261
268
  }
262
269
 
263
- const requestBodyStr = JSON.stringify({
264
- algorithm: DefaultParams.defaultECAlgorithm,
265
- // nano keyAccess minimum, header is used for nano
270
+ const unsignedRequest = create(UnsignedRewrapRequestSchema, {
271
+ clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
272
+ requests: [
273
+ create(UnsignedRewrapRequest_WithPolicyRequestSchema, {
274
+ keyAccessObjects: [
275
+ {
276
+ keyAccessObjectId: 'kao-0',
277
+ keyAccessObject: {
278
+ header: new Uint8Array(nanoTdfHeader),
279
+ kasUrl: '',
280
+ protocol: Client.KAS_PROTOCOL,
281
+ keyType: Client.KEY_ACCESS_REMOTE,
282
+ },
283
+ },
284
+ ],
285
+ algorithm: DefaultParams.defaultECAlgorithm,
286
+ }),
287
+ ],
266
288
  keyAccess: {
267
- type: Client.KEY_ACCESS_REMOTE,
268
- url: '',
289
+ header: new Uint8Array(nanoTdfHeader),
290
+ kasUrl: '',
269
291
  protocol: Client.KAS_PROTOCOL,
270
- header: base64.encodeArrayBuffer(nanoTdfHeader),
292
+ keyType: Client.KEY_ACCESS_REMOTE,
271
293
  },
272
- clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
294
+ algorithm: DefaultParams.defaultECAlgorithm,
273
295
  });
274
296
 
297
+ const requestBodyStr = toJsonString(UnsignedRewrapRequestSchema, unsignedRequest);
298
+
275
299
  const jwtPayload = { requestBody: requestBodyStr };
276
300
 
277
301
  const signedRequestToken = await reqSignature(jwtPayload, requestSignerKeyPair.privateKey, {
@@ -285,9 +309,28 @@ export default class Client {
285
309
  this.authProvider,
286
310
  this.fulfillableObligationFQNs
287
311
  );
312
+ upgradeRewrapResponseV1(rewrapResp);
313
+
314
+ // Assume only one response and one result for now (V1 style)
315
+ const result = rewrapResp.responses[0].results[0];
316
+ let entityWrappedKey: Uint8Array<ArrayBufferLike>;
317
+ switch (result.result.case) {
318
+ case 'kasWrappedKey': {
319
+ entityWrappedKey = result.result.value;
320
+ break;
321
+ }
322
+ case 'error': {
323
+ handleRpcRewrapErrorString(
324
+ result.result.value,
325
+ getPlatformUrlFromKasEndpoint(kasRewrapUrl)
326
+ );
327
+ }
328
+ default: {
329
+ throw new DecryptError('KAS rewrap response missing wrapped key');
330
+ }
331
+ }
288
332
 
289
333
  // Extract the iv and ciphertext
290
- const entityWrappedKey = rewrapResp.entityWrappedKey;
291
334
  const ivLength =
292
335
  clientVersion == Client.SDK_INITIAL_RELEASE ? Client.INITIAL_RELEASE_IV_SIZE : Client.IV_SIZE;
293
336
  const iv = entityWrappedKey.subarray(0, ivLength);
package/src/utils.ts CHANGED
@@ -3,7 +3,12 @@ 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
+ import {
7
+ RewrapResponse,
8
+ PolicyRewrapResultSchema,
9
+ KeyAccessRewrapResultSchema,
10
+ } from './platform/kas/kas_pb.js';
11
+ import { create } from '@bufbuild/protobuf';
7
12
  import { ConnectError } from '@connectrpc/connect';
8
13
 
9
14
  const REQUIRED_OBLIGATIONS_METADATA_KEY = 'X-Required-Obligations';
@@ -255,3 +260,31 @@ export function getRequiredObligationFQNs(response: RewrapResponse) {
255
260
 
256
261
  return [...requiredObligations.values()];
257
262
  }
263
+
264
+ /**
265
+ * Upgrades a RewrapResponse from v1 format to v2.
266
+ */
267
+ export function upgradeRewrapResponseV1(response: RewrapResponse) {
268
+ if (response.responses.length > 0) {
269
+ return;
270
+ }
271
+ if (response.entityWrappedKey.length === 0) {
272
+ return;
273
+ }
274
+
275
+ response.responses = [
276
+ create(PolicyRewrapResultSchema, {
277
+ policyId: 'policy',
278
+ results: [
279
+ create(KeyAccessRewrapResultSchema, {
280
+ keyAccessObjectId: 'kao-0',
281
+ status: 'permit',
282
+ result: {
283
+ case: 'kasWrappedKey',
284
+ value: response.entityWrappedKey,
285
+ },
286
+ }),
287
+ ],
288
+ }),
289
+ ];
290
+ }
package/tdf3/src/tdf.ts CHANGED
@@ -8,7 +8,16 @@ import {
8
8
  fetchWrappedKey,
9
9
  publicKeyAlgorithmToJwa,
10
10
  } from '../../src/access.js';
11
+ import { create, toJsonString } from '@bufbuild/protobuf';
12
+ import {
13
+ KeyAccessSchema,
14
+ UnsignedRewrapRequestSchema,
15
+ UnsignedRewrapRequest_WithPolicyRequestSchema,
16
+ UnsignedRewrapRequest_WithPolicySchema,
17
+ UnsignedRewrapRequest_WithKeyAccessObjectSchema,
18
+ } from '../../src/platform/kas/kas_pb.js';
11
19
  import { type AuthProvider, reqSignature } from '../../src/auth/auth.js';
20
+ import { handleRpcRewrapErrorString } from '../../src/access/access-rpc.js';
12
21
  import { allPool, anyPool } from '../../src/concurrency.js';
13
22
  import { base64, hex } from '../../src/encodings/index.js';
14
23
  import {
@@ -55,7 +64,11 @@ import { ZipReader, ZipWriter, keyMerge, concatUint8, buffToString } from './uti
55
64
  import { CentralDirectory } from './utils/zip-reader.js';
56
65
  import { ztdfSalt } from './crypto/salt.js';
57
66
  import { Payload } from './models/payload.js';
58
- import { getRequiredObligationFQNs } from '../../src/utils.js';
67
+ import {
68
+ getRequiredObligationFQNs,
69
+ upgradeRewrapResponseV1,
70
+ getPlatformUrlFromKasEndpoint,
71
+ } from '../../src/utils.js';
59
72
 
60
73
  // TODO: input validation on manifest JSON
61
74
  const DEFAULT_SEGMENT_SIZE = 1024 * 1024;
@@ -189,11 +202,6 @@ export type RewrapRequest = {
189
202
 
190
203
  export type KasPublicKeyFormat = 'pkcs8' | 'jwks';
191
204
 
192
- export type RewrapResponse = {
193
- entityWrappedKey: string;
194
- sessionPublicKey: string;
195
- };
196
-
197
205
  /**
198
206
  * If we have KAS url but not public key we can fetch it from KAS, fetching
199
207
  * the value from `${kas}/kas_public_key`.
@@ -783,13 +791,50 @@ async function unwrapKey({
783
791
 
784
792
  const clientPublicKey = ephemeralEncryptionKeys.publicKey;
785
793
 
786
- const requestBodyStr = JSON.stringify({
794
+ // Convert keySplitInfo to protobuf KeyAccess
795
+ const keyAccessProto = create(KeyAccessSchema, {
796
+ ...(keySplitInfo.type && { keyType: keySplitInfo.type }),
797
+ ...(keySplitInfo.url && { kasUrl: keySplitInfo.url }),
798
+ ...(keySplitInfo.protocol && { protocol: keySplitInfo.protocol }),
799
+ ...(keySplitInfo.wrappedKey && {
800
+ wrappedKey: new Uint8Array(base64.decodeArrayBuffer(keySplitInfo.wrappedKey)),
801
+ }),
802
+ ...(keySplitInfo.policyBinding && { policyBinding: keySplitInfo.policyBinding }),
803
+ ...(keySplitInfo.kid && { kid: keySplitInfo.kid }),
804
+ ...(keySplitInfo.sid && { splitId: keySplitInfo.sid }),
805
+ ...(keySplitInfo.encryptedMetadata && { encryptedMetadata: keySplitInfo.encryptedMetadata }),
806
+ ...(keySplitInfo.ephemeralPublicKey && {
807
+ ephemeralPublicKey: keySplitInfo.ephemeralPublicKey,
808
+ }),
809
+ });
810
+
811
+ // Create the protobuf request
812
+ const unsignedRequest = create(UnsignedRewrapRequestSchema, {
813
+ clientPublicKey,
814
+ requests: [
815
+ create(UnsignedRewrapRequest_WithPolicyRequestSchema, {
816
+ keyAccessObjects: [
817
+ create(UnsignedRewrapRequest_WithKeyAccessObjectSchema, {
818
+ keyAccessObjectId: 'kao-0',
819
+ keyAccessObject: keyAccessProto,
820
+ }),
821
+ ],
822
+ ...(manifest.encryptionInformation.policy && {
823
+ policy: create(UnsignedRewrapRequest_WithPolicySchema, {
824
+ id: 'policy',
825
+ body: manifest.encryptionInformation.policy,
826
+ }),
827
+ }),
828
+ }),
829
+ ],
830
+ // include deprecated fields for backward compatibility
787
831
  algorithm: 'RS256',
788
- keyAccess: keySplitInfo,
832
+ keyAccess: keyAccessProto,
789
833
  policy: manifest.encryptionInformation.policy,
790
- clientPublicKey,
791
834
  });
792
835
 
836
+ const requestBodyStr = toJsonString(UnsignedRewrapRequestSchema, unsignedRequest);
837
+
793
838
  const jwtPayload = { requestBody: requestBodyStr };
794
839
  const signedRequestToken = await reqSignature(jwtPayload, dpopKeys.privateKey);
795
840
 
@@ -799,39 +844,57 @@ async function unwrapKey({
799
844
  authProvider,
800
845
  fulfillableObligations
801
846
  );
802
- const { entityWrappedKey, metadata, sessionPublicKey } = rewrapResp;
847
+ upgradeRewrapResponseV1(rewrapResp);
848
+ const { sessionPublicKey } = rewrapResp;
803
849
  const requiredObligations = getRequiredObligationFQNs(rewrapResp);
850
+ // Assume only one response and one result for now (V1 style)
851
+ const result = rewrapResp.responses[0].results[0];
852
+ const metadata = result.metadata;
853
+ // Handle the different cases of result.result
854
+ switch (result.result.case) {
855
+ case 'kasWrappedKey': {
856
+ const entityWrappedKey = result.result.value;
857
+
858
+ if (wrappingKeyAlgorithm === 'ec:secp256r1') {
859
+ const serverEphemeralKey: CryptoKey = await pemPublicToCrypto(sessionPublicKey);
860
+ const ekr = ephemeralEncryptionKeysRaw as CryptoKeyPair;
861
+ const kek = await keyAgreement(ekr.privateKey, serverEphemeralKey, {
862
+ hkdfSalt: await ztdfSalt,
863
+ hkdfHash: 'SHA-256',
864
+ });
865
+ const wrappedKeyAndNonce = entityWrappedKey;
866
+ const iv = wrappedKeyAndNonce.slice(0, 12);
867
+ const wrappedKey = wrappedKeyAndNonce.slice(12);
804
868
 
805
- if (wrappingKeyAlgorithm === 'ec:secp256r1') {
806
- const serverEphemeralKey: CryptoKey = await pemPublicToCrypto(sessionPublicKey);
807
- const ekr = ephemeralEncryptionKeysRaw as CryptoKeyPair;
808
- const kek = await keyAgreement(ekr.privateKey, serverEphemeralKey, {
809
- hkdfSalt: await ztdfSalt,
810
- hkdfHash: 'SHA-256',
811
- });
812
- const wrappedKeyAndNonce = entityWrappedKey;
813
- const iv = wrappedKeyAndNonce.slice(0, 12);
814
- const wrappedKey = wrappedKeyAndNonce.slice(12);
815
-
816
- const dek = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, kek, wrappedKey);
817
-
818
- return {
819
- key: new Uint8Array(dek),
820
- metadata,
821
- requiredObligations,
822
- };
823
- }
824
- const key = Binary.fromArrayBuffer(entityWrappedKey);
825
- const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
826
- key,
827
- ephemeralEncryptionKeys.privateKey
828
- );
869
+ const dek = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, kek, wrappedKey);
829
870
 
830
- return {
831
- key: new Uint8Array(decryptedKeyBinary.asByteArray()),
832
- metadata,
833
- requiredObligations,
834
- };
871
+ return {
872
+ key: new Uint8Array(dek),
873
+ metadata,
874
+ requiredObligations,
875
+ };
876
+ }
877
+ const key = Binary.fromArrayBuffer(entityWrappedKey);
878
+ const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
879
+ key,
880
+ ephemeralEncryptionKeys.privateKey
881
+ );
882
+
883
+ return {
884
+ key: new Uint8Array(decryptedKeyBinary.asByteArray()),
885
+ metadata,
886
+ requiredObligations,
887
+ };
888
+ }
889
+
890
+ case 'error': {
891
+ handleRpcRewrapErrorString(result.result.value, getPlatformUrlFromKasEndpoint(url));
892
+ }
893
+
894
+ default: {
895
+ throw new DecryptError('KAS rewrap response missing wrapped key');
896
+ }
897
+ }
835
898
  }
836
899
 
837
900
  let poolSize = 1;