@virtru/dsp-sdk 0.4.0 → 0.5.0

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 (49) hide show
  1. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_build.chunks.jsonl +4 -2
  2. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_test.chunks.jsonl +355 -13
  3. package/.rush/temp/package-deps__phase_build.json +17 -9
  4. package/.rush/temp/shrinkwrap-deps.json +148 -150
  5. package/CHANGELOG.json +42 -0
  6. package/CHANGELOG.md +15 -1
  7. package/README.md +14 -8
  8. package/dist/src/lib/fips-crypto/asymmetric.d.ts +35 -0
  9. package/dist/src/lib/fips-crypto/asymmetric.js +116 -0
  10. package/dist/src/lib/fips-crypto/key-format.d.ts +51 -0
  11. package/dist/src/lib/fips-crypto/key-format.js +233 -0
  12. package/dist/src/lib/fips-crypto/keys.d.ts +26 -0
  13. package/dist/src/lib/fips-crypto/keys.js +123 -0
  14. package/dist/src/lib/fips-crypto/primitives.d.ts +41 -0
  15. package/dist/src/lib/fips-crypto/primitives.js +105 -0
  16. package/dist/src/lib/fips-crypto/symmetric.d.ts +61 -0
  17. package/dist/src/lib/fips-crypto/symmetric.js +152 -0
  18. package/dist/src/lib/fips-crypto-service.d.ts +5 -0
  19. package/dist/src/lib/fips-crypto-service.js +44 -0
  20. package/dist/src/lib/utils.d.ts +8 -2
  21. package/dist/src/lib/utils.js +8 -9
  22. package/dist/tests/fips-crypto.unit.test.d.ts +1 -0
  23. package/dist/tests/fips-crypto.unit.test.js +258 -0
  24. package/dist/tests/setup-unit.d.ts +7 -0
  25. package/dist/tests/setup-unit.js +70 -0
  26. package/lib-commonjs/src/lib/fips-crypto/asymmetric.js +126 -0
  27. package/lib-commonjs/src/lib/fips-crypto/key-format.js +243 -0
  28. package/lib-commonjs/src/lib/fips-crypto/keys.js +134 -0
  29. package/lib-commonjs/src/lib/fips-crypto/primitives.js +112 -0
  30. package/lib-commonjs/src/lib/fips-crypto/symmetric.js +162 -0
  31. package/lib-commonjs/src/lib/fips-crypto-service.js +57 -0
  32. package/lib-commonjs/src/lib/utils.js +8 -9
  33. package/lib-commonjs/tests/fips-crypto.unit.test.js +260 -0
  34. package/lib-commonjs/tests/setup-unit.js +72 -0
  35. package/package.json +12 -7
  36. package/public/virtru.wasm +0 -0
  37. package/src/lib/fips-crypto/asymmetric.ts +162 -0
  38. package/src/lib/fips-crypto/key-format.ts +287 -0
  39. package/src/lib/fips-crypto/keys.ts +190 -0
  40. package/src/lib/fips-crypto/primitives.ts +197 -0
  41. package/src/lib/fips-crypto/symmetric.ts +213 -0
  42. package/src/lib/fips-crypto-service.ts +58 -0
  43. package/src/lib/utils.test.ts +11 -8
  44. package/src/lib/utils.ts +14 -13
  45. package/temp/build/lint/_eslint-5eVG3S6w.json +9 -1
  46. package/tests/fips-crypto.unit.test.ts +412 -0
  47. package/tests/setup-unit.ts +108 -0
  48. package/tsconfig.json +1 -1
  49. package/vitest.config.ts +22 -6
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) Virtru Corporation. All rights reserved.
4
+ *
5
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.FipsCryptoService = exports.exportPrivateKeyPem = exports.importPrivateKey = exports.unwrapSymmetricKey = exports.unwrapPrivateKey = exports.unwrapPublicKey = exports.wrapKeyPair = exports.wrapSymmetricKey = exports.wrapPrivateKey = exports.wrapPublicKey = exports.initializeFipsCrypto = void 0;
9
+ const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
10
+ var primitives_1 = require("./fips-crypto/primitives");
11
+ Object.defineProperty(exports, "initializeFipsCrypto", { enumerable: true, get: function () { return primitives_1.initializeFipsCrypto; } });
12
+ var keys_1 = require("./fips-crypto/keys");
13
+ Object.defineProperty(exports, "wrapPublicKey", { enumerable: true, get: function () { return keys_1.wrapPublicKey; } });
14
+ Object.defineProperty(exports, "wrapPrivateKey", { enumerable: true, get: function () { return keys_1.wrapPrivateKey; } });
15
+ Object.defineProperty(exports, "wrapSymmetricKey", { enumerable: true, get: function () { return keys_1.wrapSymmetricKey; } });
16
+ Object.defineProperty(exports, "wrapKeyPair", { enumerable: true, get: function () { return keys_1.wrapKeyPair; } });
17
+ Object.defineProperty(exports, "unwrapPublicKey", { enumerable: true, get: function () { return keys_1.unwrapPublicKey; } });
18
+ Object.defineProperty(exports, "unwrapPrivateKey", { enumerable: true, get: function () { return keys_1.unwrapPrivateKey; } });
19
+ Object.defineProperty(exports, "unwrapSymmetricKey", { enumerable: true, get: function () { return keys_1.unwrapSymmetricKey; } });
20
+ const primitives_2 = require("./fips-crypto/primitives");
21
+ const symmetric_1 = require("./fips-crypto/symmetric");
22
+ const asymmetric_1 = require("./fips-crypto/asymmetric");
23
+ const key_format_1 = require("./fips-crypto/key-format");
24
+ var key_format_2 = require("./fips-crypto/key-format");
25
+ Object.defineProperty(exports, "importPrivateKey", { enumerable: true, get: function () { return key_format_2.importPrivateKey; } });
26
+ Object.defineProperty(exports, "exportPrivateKeyPem", { enumerable: true, get: function () { return key_format_2.exportPrivateKeyPem; } });
27
+ // ─── FipsCryptoService ────────────────────────────────────────────────────────
28
+ exports.FipsCryptoService = {
29
+ name: 'FipsCryptoService',
30
+ method: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
31
+ decrypt: symmetric_1.decrypt,
32
+ decryptWithPrivateKey: asymmetric_1.decryptWithPrivateKey,
33
+ encrypt: symmetric_1.encrypt,
34
+ encryptWithPublicKey: asymmetric_1.encryptWithPublicKey,
35
+ generateKey: symmetric_1.generateKey,
36
+ generateKeyPair: asymmetric_1.generateKeyPair,
37
+ generateSigningKeyPair: asymmetric_1.generateSigningKeyPair,
38
+ randomBytes: primitives_2.randomBytes,
39
+ sign: asymmetric_1.sign,
40
+ verify: asymmetric_1.verify,
41
+ hmac: symmetric_1.hmac,
42
+ verifyHmac: symmetric_1.verifyHmac,
43
+ digest: primitives_2.digest,
44
+ generateECKeyPair: asymmetric_1.generateECKeyPair,
45
+ deriveKeyFromECDH: asymmetric_1.deriveKeyFromECDH,
46
+ importPublicKey: key_format_1.importPublicKey,
47
+ importPrivateKey: key_format_1.importPrivateKey,
48
+ exportPublicKeyPem: key_format_1.exportPublicKeyPem,
49
+ exportPrivateKeyPem: key_format_1.exportPrivateKeyPem,
50
+ exportPublicKeyJwk: key_format_1.exportPublicKeyJwk,
51
+ parsePublicKeyPem: key_format_1.parsePublicKeyPem,
52
+ extractPublicKeyPem: key_format_1.extractPublicKeyPem,
53
+ jwkToPublicKeyPem: key_format_1.jwkToPublicKeyPem,
54
+ importSymmetricKey: symmetric_1.importSymmetricKey,
55
+ splitSymmetricKey: symmetric_1.splitSymmetricKey,
56
+ mergeSymmetricKeys: symmetric_1.mergeSymmetricKeys,
57
+ };
@@ -13,6 +13,7 @@ exports.getUserEntitlements = getUserEntitlements;
13
13
  exports.getTrustedCertificates = getTrustedCertificates;
14
14
  exports.validateJWSCertificateChain = validateJWSCertificateChain;
15
15
  const sdk_1 = require("@opentdf/sdk");
16
+ const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
16
17
  const platform_1 = require("@opentdf/sdk/platform");
17
18
  const common_pb_js_1 = require("@opentdf/sdk/platform/common/common_pb.js");
18
19
  const asn1js = require("asn1js");
@@ -30,7 +31,7 @@ const client_1 = require("./client");
30
31
  * @param authProvider - An instance of `AuthProvider` used to generate authentication credentials.
31
32
  * @returns An `Interceptor` function that modifies requests to include authentication headers.
32
33
  */
33
- function createAuthInterceptor(authProvider) {
34
+ function createAuthInterceptor(authProvider, _cryptoService) {
34
35
  return (next) => async (req) => {
35
36
  const url = new URL(req.url);
36
37
  const pathOnly = url.pathname;
@@ -76,6 +77,7 @@ async function createOpenTDFClient(options) {
76
77
  noVerify: true,
77
78
  },
78
79
  disableDPoP: true,
80
+ cryptoService: options.cryptoService,
79
81
  });
80
82
  }
81
83
  const getObligations = async (ciphertext, options) => {
@@ -102,6 +104,7 @@ async function getUserEntitlements(options) {
102
104
  const authProvider = await createAuthProvider({
103
105
  oidc: options.oidc,
104
106
  refreshToken: options.refreshToken,
107
+ cryptoService: options.cryptoService,
105
108
  });
106
109
  const accessToken = await authProvider.oidcAuth.get();
107
110
  const userEntitlementsResponse = await fetch(`${options.platformEndpoint}/shared/entitlements`, {
@@ -185,17 +188,13 @@ async function getCertificatesForNamespace(dspClient, namespaceId) {
185
188
  * ```
186
189
  */
187
190
  async function getTrustedCertificates(options) {
188
- function generateSigningKeyPair() {
189
- return crypto.subtle.generateKey({
190
- name: 'ECDSA',
191
- namedCurve: 'P-256',
192
- }, true, ['sign', 'verify']);
193
- }
191
+ const cs = options.cryptoService ?? singlecontainer_1.WebCryptoService;
194
192
  const authProvider = await createAuthProvider({
195
193
  oidc: options.oidc,
196
194
  refreshToken: options.refreshToken,
195
+ cryptoService: options.cryptoService,
197
196
  });
198
- await authProvider.updateClientPublicKey(await generateSigningKeyPair());
197
+ await authProvider.updateClientPublicKey(await cs.generateSigningKeyPair());
199
198
  // Create both clients
200
199
  const platform = new platform_1.PlatformClient({
201
200
  authProvider,
@@ -311,7 +310,7 @@ function pemToArrayBuffer(pem) {
311
310
  * }
312
311
  * ```
313
312
  */
314
- async function validateJWSCertificateChain(x5c, trustedRootCertificates) {
313
+ async function validateJWSCertificateChain(x5c, trustedRootCertificates, _cryptoService) {
315
314
  const warnings = [];
316
315
  try {
317
316
  // Validate inputs
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) Virtru Corporation. All rights reserved.
4
+ *
5
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const vitest_1 = require("vitest");
9
+ const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
10
+ const fips_crypto_service_1 = require("../src/lib/fips-crypto-service");
11
+ (0, vitest_1.describe)('FIPS CryptoService - Methods', () => {
12
+ // RSA key slots are finite in the WASM keystore. Each describe group that
13
+ // uses RSA keys creates ONE shared key pair in beforeAll and releases it in
14
+ // afterAll so slots are reclaimed before the next group starts.
15
+ (0, vitest_1.describe)('sign and verify (asymmetric)', () => {
16
+ let keyPair;
17
+ (0, vitest_1.beforeAll)(async () => {
18
+ keyPair = await fips_crypto_service_1.FipsCryptoService.generateSigningKeyPair();
19
+ }, 60000);
20
+ (0, vitest_1.afterAll)(() => {
21
+ (0, fips_crypto_service_1.unwrapPublicKey)(keyPair.publicKey).release();
22
+ });
23
+ (0, vitest_1.test)('should sign and verify with RS256', async () => {
24
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
25
+ const signature = await fips_crypto_service_1.FipsCryptoService.sign(data, keyPair.privateKey, 'RS256');
26
+ const isValid = await fips_crypto_service_1.FipsCryptoService.verify(data, signature, keyPair.publicKey, 'RS256');
27
+ (0, vitest_1.expect)(isValid).toBe(true);
28
+ });
29
+ (0, vitest_1.test)('should reject tampered data', async () => {
30
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
31
+ const signature = await fips_crypto_service_1.FipsCryptoService.sign(data, keyPair.privateKey, 'RS256');
32
+ const tamperedData = new Uint8Array([1, 2, 3, 4, 6]);
33
+ const isValid = await fips_crypto_service_1.FipsCryptoService.verify(tamperedData, signature, keyPair.publicKey, 'RS256');
34
+ (0, vitest_1.expect)(isValid).toBe(false);
35
+ });
36
+ (0, vitest_1.test)('should throw error for ES256 (EC not supported)', async () => {
37
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.sign(new Uint8Array([1, 2, 3, 4]), keyPair.privateKey, 'ES256')).rejects.toThrow('not supported in FIPS mode');
38
+ });
39
+ (0, vitest_1.test)('encryption key pair (RSA-OAEP) should not be usable for signing', async () => {
40
+ // Catches algorithm mismatch: if generateKeyPair() incorrectly used
41
+ // RSASSA-PKCS1-v1_5 params, signing would succeed here instead of throwing.
42
+ const encKeyPair = await fips_crypto_service_1.FipsCryptoService.generateKeyPair();
43
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.sign(new Uint8Array([1, 2, 3]), encKeyPair.privateKey, 'RS256')).rejects.toThrow();
44
+ (0, fips_crypto_service_1.unwrapPublicKey)(encKeyPair.publicKey).release();
45
+ (0, fips_crypto_service_1.unwrapPrivateKey)(encKeyPair.privateKey).release();
46
+ });
47
+ });
48
+ (0, vitest_1.describe)('hmac and verifyHmac', () => {
49
+ (0, vitest_1.test)('should compute and verify HMAC-SHA256', async () => {
50
+ const crypto = fips_crypto_service_1.FipsCryptoService;
51
+ const keyBytes = await crypto.randomBytes(32);
52
+ const symKey = await crypto.importSymmetricKey(keyBytes);
53
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
54
+ const signature = await crypto.hmac(data, symKey);
55
+ const isValid = await crypto.verifyHmac(data, signature, symKey);
56
+ (0, vitest_1.expect)(isValid).toBe(true);
57
+ });
58
+ (0, vitest_1.test)('should reject invalid signature', async () => {
59
+ const crypto = fips_crypto_service_1.FipsCryptoService;
60
+ const symKey = await crypto.generateKey(32);
61
+ const data = new Uint8Array([1, 2, 3, 4]);
62
+ const signature = await crypto.hmac(data, symKey);
63
+ signature[0] ^= 1; // Flip a bit
64
+ const isValid = await crypto.verifyHmac(data, signature, symKey);
65
+ (0, vitest_1.expect)(isValid).toBe(false);
66
+ });
67
+ (0, vitest_1.test)('should reject signature with wrong key', async () => {
68
+ const crypto = fips_crypto_service_1.FipsCryptoService;
69
+ const symKey1 = await crypto.importSymmetricKey(await crypto.randomBytes(32));
70
+ const symKey2 = await crypto.importSymmetricKey(await crypto.randomBytes(32));
71
+ const data = new Uint8Array([1, 2, 3, 4]);
72
+ const signature = await crypto.hmac(data, symKey1);
73
+ const isValid = await crypto.verifyHmac(data, signature, symKey2);
74
+ (0, vitest_1.expect)(isValid).toBe(false);
75
+ });
76
+ });
77
+ (0, vitest_1.describe)('digest', () => {
78
+ (0, vitest_1.test)('should compute SHA-256 hash', async () => {
79
+ const data = new Uint8Array([1, 2, 3, 4]);
80
+ const hash = await fips_crypto_service_1.FipsCryptoService.digest('SHA-256', data);
81
+ (0, vitest_1.expect)(hash).toBeInstanceOf(Uint8Array);
82
+ (0, vitest_1.expect)(hash.byteLength).toBe(32);
83
+ });
84
+ (0, vitest_1.test)('should produce consistent hashes', async () => {
85
+ const data = new Uint8Array([1, 2, 3, 4]);
86
+ const hash1 = await fips_crypto_service_1.FipsCryptoService.digest('SHA-256', data);
87
+ const hash2 = await fips_crypto_service_1.FipsCryptoService.digest('SHA-256', data);
88
+ (0, vitest_1.expect)(hash1).toEqual(hash2);
89
+ });
90
+ (0, vitest_1.test)('should throw error for SHA-384 (not supported)', async () => {
91
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.digest('SHA-384', new Uint8Array([1, 2, 3, 4]))).rejects.toThrow('not supported in FIPS mode');
92
+ });
93
+ });
94
+ (0, vitest_1.describe)('EC placeholder methods', () => {
95
+ (0, vitest_1.test)('should throw error for generateECKeyPair', async () => {
96
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.generateECKeyPair()).rejects.toThrow('not supported in FIPS mode');
97
+ });
98
+ (0, vitest_1.test)('should throw error for deriveKeyFromECDH', async () => {
99
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.deriveKeyFromECDH({}, {}, {
100
+ hash: 'SHA-256',
101
+ salt: new Uint8Array(),
102
+ })).rejects.toThrow('not supported in FIPS mode');
103
+ });
104
+ });
105
+ (0, vitest_1.describe)('exportPublicKeyPem', () => {
106
+ let keyPair;
107
+ (0, vitest_1.beforeAll)(async () => {
108
+ keyPair = await fips_crypto_service_1.FipsCryptoService.generateKeyPair();
109
+ }, 60000);
110
+ (0, vitest_1.afterAll)(() => {
111
+ (0, fips_crypto_service_1.unwrapPublicKey)(keyPair.publicKey).release();
112
+ });
113
+ (0, vitest_1.test)('should export generated public key to PEM', async () => {
114
+ const pem = await fips_crypto_service_1.FipsCryptoService.exportPublicKeyPem(keyPair.publicKey);
115
+ (0, vitest_1.expect)(pem).toContain('-----BEGIN PUBLIC KEY-----');
116
+ (0, vitest_1.expect)(pem).toContain('-----END PUBLIC KEY-----');
117
+ });
118
+ });
119
+ (0, vitest_1.describe)('importPublicKey', () => {
120
+ let keyPair;
121
+ let importedKey;
122
+ (0, vitest_1.beforeAll)(async () => {
123
+ const crypto = fips_crypto_service_1.FipsCryptoService;
124
+ keyPair = await crypto.generateKeyPair(2048);
125
+ const pem = await crypto.exportPublicKeyPem(keyPair.publicKey);
126
+ importedKey = await crypto.importPublicKey(pem, { usage: 'encrypt' });
127
+ }, 60000);
128
+ (0, vitest_1.afterAll)(() => {
129
+ (0, fips_crypto_service_1.unwrapPublicKey)(keyPair.publicKey).release();
130
+ (0, fips_crypto_service_1.unwrapPublicKey)(importedKey).release();
131
+ });
132
+ (0, vitest_1.test)('should import PEM and detect RSA-2048', () => {
133
+ (0, vitest_1.expect)(importedKey.algorithm).toBe('rsa:2048');
134
+ });
135
+ });
136
+ (0, vitest_1.describe)('exportPublicKeyJwk', () => {
137
+ let keyPair;
138
+ (0, vitest_1.beforeAll)(async () => {
139
+ keyPair = await fips_crypto_service_1.FipsCryptoService.generateKeyPair(2048);
140
+ }, 60000);
141
+ (0, vitest_1.afterAll)(() => {
142
+ (0, fips_crypto_service_1.unwrapPublicKey)(keyPair.publicKey).release();
143
+ });
144
+ (0, vitest_1.test)('should export public key to JWK', async () => {
145
+ const jwk = await fips_crypto_service_1.FipsCryptoService.exportPublicKeyJwk(keyPair.publicKey);
146
+ (0, vitest_1.expect)(jwk.kty).toBe('RSA');
147
+ (0, vitest_1.expect)(jwk.n).toBeDefined();
148
+ (0, vitest_1.expect)(jwk.e).toBeDefined();
149
+ (0, vitest_1.expect)(typeof jwk.n).toBe('string');
150
+ (0, vitest_1.expect)(typeof jwk.e).toBe('string');
151
+ });
152
+ (0, vitest_1.test)('should produce a JWK that round-trips back to SPKI', async () => {
153
+ const jwk = await fips_crypto_service_1.FipsCryptoService.exportPublicKeyJwk(keyPair.publicKey);
154
+ const webKey = await globalThis.crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify']);
155
+ const spki = await globalThis.crypto.subtle.exportKey('spki', webKey);
156
+ (0, vitest_1.expect)(spki.byteLength).toBeGreaterThan(0);
157
+ });
158
+ });
159
+ (0, vitest_1.describe)('encrypt and decrypt (symmetric AES-GCM)', () => {
160
+ (0, vitest_1.test)('should round-trip encrypt/decrypt', async () => {
161
+ const key = await fips_crypto_service_1.FipsCryptoService.generateKey();
162
+ const iv = singlecontainer_1.Binary.fromArrayBuffer((await fips_crypto_service_1.FipsCryptoService.randomBytes(12)).buffer);
163
+ const plaintext = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
164
+ const { payload: cipher, authTag } = await fips_crypto_service_1.FipsCryptoService.encrypt(singlecontainer_1.Binary.fromArrayBuffer(plaintext.buffer), key, iv);
165
+ const { payload: recovered } = await fips_crypto_service_1.FipsCryptoService.decrypt(cipher, key, iv, undefined, authTag);
166
+ (0, vitest_1.expect)(new Uint8Array(recovered.asArrayBuffer())).toEqual(plaintext);
167
+ });
168
+ (0, vitest_1.test)('should produce different ciphertext for different IVs', async () => {
169
+ const key = await fips_crypto_service_1.FipsCryptoService.generateKey();
170
+ const plaintext = singlecontainer_1.Binary.fromArrayBuffer(new Uint8Array([1, 2, 3, 4]).buffer);
171
+ const iv1 = singlecontainer_1.Binary.fromArrayBuffer((await fips_crypto_service_1.FipsCryptoService.randomBytes(12)).buffer);
172
+ const iv2 = singlecontainer_1.Binary.fromArrayBuffer((await fips_crypto_service_1.FipsCryptoService.randomBytes(12)).buffer);
173
+ const { payload: cipher1 } = await fips_crypto_service_1.FipsCryptoService.encrypt(plaintext, key, iv1);
174
+ const { payload: cipher2 } = await fips_crypto_service_1.FipsCryptoService.encrypt(plaintext, key, iv2);
175
+ (0, vitest_1.expect)(new Uint8Array(cipher1.asArrayBuffer())).not.toEqual(new Uint8Array(cipher2.asArrayBuffer()));
176
+ });
177
+ });
178
+ (0, vitest_1.describe)('encryptWithPublicKey and decryptWithPrivateKey (RSA-OAEP)', () => {
179
+ let keyPair;
180
+ (0, vitest_1.beforeAll)(async () => {
181
+ keyPair = await fips_crypto_service_1.FipsCryptoService.generateKeyPair();
182
+ }, 60000);
183
+ (0, vitest_1.afterAll)(() => {
184
+ (0, fips_crypto_service_1.unwrapPublicKey)(keyPair.publicKey).release();
185
+ });
186
+ (0, vitest_1.test)('should round-trip RSA encrypt/decrypt', async () => {
187
+ const plaintext = new Uint8Array([10, 20, 30, 40, 50]);
188
+ const encrypted = await fips_crypto_service_1.FipsCryptoService.encryptWithPublicKey(singlecontainer_1.Binary.fromArrayBuffer(plaintext.buffer), keyPair.publicKey);
189
+ const decrypted = await fips_crypto_service_1.FipsCryptoService.decryptWithPrivateKey(encrypted, keyPair.privateKey);
190
+ (0, vitest_1.expect)(new Uint8Array(decrypted.asArrayBuffer())).toEqual(plaintext);
191
+ });
192
+ (0, vitest_1.test)('should produce different ciphertext each call (RSA-OAEP is randomized)', async () => {
193
+ const payload = singlecontainer_1.Binary.fromArrayBuffer(new Uint8Array([1, 2, 3]).buffer);
194
+ const enc1 = await fips_crypto_service_1.FipsCryptoService.encryptWithPublicKey(payload, keyPair.publicKey);
195
+ const enc2 = await fips_crypto_service_1.FipsCryptoService.encryptWithPublicKey(payload, keyPair.publicKey);
196
+ (0, vitest_1.expect)(new Uint8Array(enc1.asArrayBuffer())).not.toEqual(new Uint8Array(enc2.asArrayBuffer()));
197
+ });
198
+ (0, vitest_1.test)('signing key pair (RSASSA-PKCS1-v1_5) should not be usable for encryption', async () => {
199
+ // Catches algorithm mismatch: if generateSigningKeyPair() incorrectly used
200
+ // RSA-OAEP params, encryption would succeed here instead of throwing.
201
+ const signingKp = await fips_crypto_service_1.FipsCryptoService.generateSigningKeyPair();
202
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.encryptWithPublicKey(singlecontainer_1.Binary.fromArrayBuffer(new Uint8Array([1, 2, 3]).buffer), signingKp.publicKey)).rejects.toThrow();
203
+ (0, fips_crypto_service_1.unwrapPublicKey)(signingKp.publicKey).release();
204
+ (0, fips_crypto_service_1.unwrapPrivateKey)(signingKp.privateKey).release();
205
+ });
206
+ });
207
+ (0, vitest_1.describe)('importPrivateKey and exportPrivateKeyPem', () => {
208
+ let privKey;
209
+ (0, vitest_1.beforeAll)(async () => {
210
+ const kp = await globalThis.crypto.subtle.generateKey({
211
+ name: 'RSASSA-PKCS1-v1_5',
212
+ modulusLength: 2048,
213
+ publicExponent: new Uint8Array([1, 0, 1]),
214
+ hash: 'SHA-256',
215
+ }, true, ['sign', 'verify']);
216
+ const pkcs8 = await globalThis.crypto.subtle.exportKey('pkcs8', kp.privateKey);
217
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(pkcs8)));
218
+ const pem = `-----BEGIN PRIVATE KEY-----\n${b64
219
+ .match(/.{1,64}/g)
220
+ .join('\n')}\n-----END PRIVATE KEY-----`;
221
+ privKey = await (0, fips_crypto_service_1.importPrivateKey)(pem, {
222
+ usage: 'sign',
223
+ extractable: true,
224
+ });
225
+ }, 60000);
226
+ (0, vitest_1.afterAll)(() => {
227
+ (0, fips_crypto_service_1.unwrapPrivateKey)(privKey).release();
228
+ });
229
+ (0, vitest_1.test)('should import PKCS#8 PEM private key', () => {
230
+ (0, vitest_1.expect)(privKey.algorithm).toBe('rsa:2048');
231
+ });
232
+ (0, vitest_1.test)('should export imported private key back to PEM', async () => {
233
+ const exported = await (0, fips_crypto_service_1.exportPrivateKeyPem)(privKey);
234
+ (0, vitest_1.expect)(exported).toContain('-----BEGIN PRIVATE KEY-----');
235
+ (0, vitest_1.expect)(exported).toContain('-----END PRIVATE KEY-----');
236
+ });
237
+ });
238
+ (0, vitest_1.describe)('splitSymmetricKey and mergeSymmetricKeys', () => {
239
+ (0, vitest_1.test)('splitSymmetricKey with 1 share returns the same key (single-KAS identity)', async () => {
240
+ const key = await fips_crypto_service_1.FipsCryptoService.generateKey();
241
+ const shares = await fips_crypto_service_1.FipsCryptoService.splitSymmetricKey(key, 1);
242
+ (0, vitest_1.expect)(shares).toHaveLength(1);
243
+ (0, vitest_1.expect)(shares[0]).toBe(key);
244
+ });
245
+ (0, vitest_1.test)('mergeSymmetricKeys with 1 share returns the same key (single-KAS identity)', async () => {
246
+ const key = await fips_crypto_service_1.FipsCryptoService.generateKey();
247
+ const merged = await fips_crypto_service_1.FipsCryptoService.mergeSymmetricKeys([key]);
248
+ (0, vitest_1.expect)(merged).toBe(key);
249
+ });
250
+ (0, vitest_1.test)('splitSymmetricKey with 2 shares throws (multi-KAS not yet supported)', async () => {
251
+ const key = await fips_crypto_service_1.FipsCryptoService.generateKey();
252
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.splitSymmetricKey(key, 2)).rejects.toThrow('not yet supported in FIPS mode');
253
+ });
254
+ (0, vitest_1.test)('mergeSymmetricKeys with 2 shares throws (multi-KAS not yet supported)', async () => {
255
+ const key1 = await fips_crypto_service_1.FipsCryptoService.generateKey();
256
+ const key2 = await fips_crypto_service_1.FipsCryptoService.generateKey();
257
+ await (0, vitest_1.expect)(fips_crypto_service_1.FipsCryptoService.mergeSymmetricKeys([key1, key2])).rejects.toThrow('not yet supported in FIPS mode');
258
+ });
259
+ });
260
+ });
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) Virtru Corporation. All rights reserved.
4
+ *
5
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const buffer_1 = require("buffer");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_path_1 = require("node:path");
11
+ const node_util_1 = require("node:util");
12
+ globalThis.Buffer = buffer_1.Buffer;
13
+ if (!globalThis.TextEncoder) {
14
+ globalThis.TextEncoder = node_util_1.TextEncoder;
15
+ }
16
+ if (!globalThis.TextDecoder) {
17
+ globalThis.TextDecoder = node_util_1.TextDecoder;
18
+ }
19
+ if (typeof window !== 'undefined' && !window.Module) {
20
+ window.Module = {};
21
+ }
22
+ // Prefer the copied local WASM artifact used by package scripts.
23
+ const copiedWasmFilePath = node_path_1.default.resolve(process.cwd(), 'public/virtru.wasm');
24
+ // Fallback for direct local runs when copy-wasm has not been executed.
25
+ const sourceWasmFilePath = node_path_1.default.resolve(process.cwd(), '../fips-crypto-js/src/utils/virtru.wasm');
26
+ let resolvedWasmFilePath = copiedWasmFilePath;
27
+ try {
28
+ await (0, promises_1.access)(copiedWasmFilePath);
29
+ }
30
+ catch {
31
+ resolvedWasmFilePath = sourceWasmFilePath;
32
+ }
33
+ const originalFetch = globalThis.fetch;
34
+ globalThis.fetch = async function (input, init) {
35
+ const url = typeof input === 'string'
36
+ ? input
37
+ : input instanceof URL
38
+ ? input.href
39
+ : input.url;
40
+ if (url && url.includes('virtru.wasm')) {
41
+ const wasmBytes = await (0, promises_1.readFile)(resolvedWasmFilePath);
42
+ return new Response(wasmBytes, {
43
+ status: 200,
44
+ headers: {
45
+ 'content-type': 'application/wasm',
46
+ },
47
+ });
48
+ }
49
+ return originalFetch(input, init);
50
+ };
51
+ await Promise.resolve().then(() => require('@virtru-private/fips-crypto-js'));
52
+ const isFipsReady = (candidate) => {
53
+ return (!!candidate &&
54
+ typeof candidate === 'object' &&
55
+ 'isInitialized' in candidate &&
56
+ candidate.isInitialized === true &&
57
+ 'generateKey' in candidate &&
58
+ typeof candidate.generateKey === 'function');
59
+ };
60
+ if (!isFipsReady(window.VirtruFipsWasmLib)) {
61
+ await new Promise((resolve, reject) => {
62
+ const timeout = setTimeout(() => reject(new Error('Timeout waiting for VirtruCrypto WASM initialization in unit setup.')), 10000);
63
+ const onReady = () => {
64
+ clearTimeout(timeout);
65
+ resolve();
66
+ };
67
+ window.addEventListener('wasmReady', onReady, { once: true });
68
+ });
69
+ }
70
+ if (!isFipsReady(window.VirtruFipsWasmLib)) {
71
+ throw new Error('VirtruCrypto was loaded but not initialized in unit setup.');
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virtru/dsp-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "main": "dist/src/index.js",
6
6
  "exports": {
@@ -20,8 +20,11 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@eslint/js": "~9.39.2",
23
+ "@rushstack/heft": "^1.1.14",
24
+ "@rushstack/heft-web-rig": "^1.3.0",
23
25
  "@types/node": "^22.0.0",
24
26
  "@vitest/browser": "~3.1.3",
27
+ "buffer": "^6.0.3",
25
28
  "eslint": "^9.17.0",
26
29
  "eslint-plugin-prettier": "^5.2.1",
27
30
  "globals": "^16.3.0",
@@ -32,15 +35,14 @@
32
35
  "typescript-eslint": "^8.39.1",
33
36
  "vite": "^5.4.21",
34
37
  "vite-plugin-dts": "^4.5.4",
35
- "vitest": "~3.1.3"
38
+ "vitest": "~3.1.3",
39
+ "@virtru-private/fips-crypto-js": "0.3.0"
36
40
  },
37
41
  "dependencies": {
38
42
  "@bufbuild/protobuf": "^2.7.0",
39
43
  "@connectrpc/connect": "~2.0.2",
40
44
  "@connectrpc/connect-web": "~2.0.2",
41
- "@opentdf/sdk": "0.9.0-rc.82",
42
- "@rushstack/heft": "^1.1.14",
43
- "@rushstack/heft-web-rig": "^1.3.0",
45
+ "@opentdf/sdk": "0.10.0-rc.97",
44
46
  "asn1js": "^3.0.5",
45
47
  "pkijs": "^3.0.19"
46
48
  },
@@ -48,9 +50,12 @@
48
50
  "build": "heft build --clean",
49
51
  "lint": "eslint .",
50
52
  "lint:fix": "eslint . --fix",
51
- "test": "vitest --run",
53
+ "test": "npm run test:unit",
54
+ "test:unit": "npm run build-fips-crypto && npm run copy-wasm && npx vitest --run --config vitest.config.ts",
52
55
  "update-protos": "./update-protos.sh",
56
+ "build-fips-crypto": "npm run build --prefix ../fips-crypto-js",
57
+ "copy-wasm": "mkdir -p public && cp ../fips-crypto-js/src/utils/virtru.wasm public/virtru.wasm",
53
58
  "_phase:build": "heft build --clean",
54
- "_phase:test": "npm run test && heft test"
59
+ "_phase:test": "npm run build-fips-crypto && npm run copy-wasm && npm run test:unit && heft test"
55
60
  }
56
61
  }
Binary file