@virtru/dsp-sdk 0.4.1 → 0.6.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 (123) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +81 -3
  3. package/dist/src/fips.d.ts +7 -0
  4. package/dist/src/fips.js +12 -0
  5. package/dist/src/lib/dsp.d.ts +17 -1
  6. package/dist/src/lib/dsp.js +28 -5
  7. package/dist/src/lib/fips-crypto/asymmetric.d.ts +35 -0
  8. package/dist/src/lib/fips-crypto/asymmetric.js +116 -0
  9. package/dist/src/lib/fips-crypto/key-format.d.ts +51 -0
  10. package/dist/src/lib/fips-crypto/key-format.js +233 -0
  11. package/dist/src/lib/fips-crypto/keys.d.ts +26 -0
  12. package/dist/src/lib/fips-crypto/keys.js +123 -0
  13. package/dist/src/lib/fips-crypto/primitives.d.ts +41 -0
  14. package/dist/src/lib/fips-crypto/primitives.js +105 -0
  15. package/dist/src/lib/fips-crypto/symmetric.d.ts +61 -0
  16. package/dist/src/lib/fips-crypto/symmetric.js +152 -0
  17. package/dist/src/lib/fips-crypto-service.d.ts +5 -0
  18. package/dist/src/lib/fips-crypto-service.js +44 -0
  19. package/dist/src/lib/utils.d.ts +8 -2
  20. package/dist/src/lib/utils.js +8 -9
  21. package/package.json +31 -5
  22. package/.prettierrc +0 -3
  23. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_build.chunks.jsonl +0 -12
  24. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_test.chunks.jsonl +0 -43
  25. package/.rush/temp/package-deps__phase_build.json +0 -68
  26. package/.rush/temp/shrinkwrap-deps.json +0 -1257
  27. package/CHANGELOG.json +0 -278
  28. package/buf.gen.yaml +0 -17
  29. package/buf.yaml +0 -4
  30. package/config/jest.config.json +0 -6
  31. package/config/rig.json +0 -8
  32. package/dist/tests/dsp-client.test.d.ts +0 -1
  33. package/dist/tests/dsp-client.test.js +0 -73
  34. package/dist/tests/dsp.test.d.ts +0 -1
  35. package/dist/tests/dsp.test.js +0 -92
  36. package/dist/tests/mocks/create-export-artifacts.d.ts +0 -2
  37. package/dist/tests/mocks/create-export-artifacts.js +0 -13
  38. package/dist/tests/mocks/tagging-pdp-tag.d.ts +0 -2
  39. package/dist/tests/mocks/tagging-pdp-tag.js +0 -11
  40. package/dist/tests/mocks/well-known-configuration.d.ts +0 -2
  41. package/dist/tests/mocks/well-known-configuration.js +0 -12
  42. package/dist/tests/setup-msw.d.ts +0 -9
  43. package/dist/tests/setup-msw.js +0 -30
  44. package/eslint.config.mjs +0 -28
  45. package/lib-commonjs/src/gen/buf/validate/validate_pb.js +0 -487
  46. package/lib-commonjs/src/gen/config/v1/config_pb.js +0 -142
  47. package/lib-commonjs/src/gen/config/v1/kas_config_pb.js +0 -72
  48. package/lib-commonjs/src/gen/config/v1/meta_pb.js +0 -36
  49. package/lib-commonjs/src/gen/config/v1/outlook_config_pb.js +0 -67
  50. package/lib-commonjs/src/gen/config/v1/secureviewer_config_pb.js +0 -67
  51. package/lib-commonjs/src/gen/config/v1/tagging_pb.js +0 -246
  52. package/lib-commonjs/src/gen/google/api/annotations_pb.js +0 -33
  53. package/lib-commonjs/src/gen/google/api/http_pb.js +0 -44
  54. package/lib-commonjs/src/gen/policyimportexport/v1/policy_import_export_pb.js +0 -93
  55. package/lib-commonjs/src/gen/shared/v1/shared_pb.js +0 -95
  56. package/lib-commonjs/src/gen/tagging/pdp/v2/tagging_pb.js +0 -277
  57. package/lib-commonjs/src/gen/version/v1/version_pb.js +0 -40
  58. package/lib-commonjs/src/gen/virtru/common/common_pb.js +0 -76
  59. package/lib-commonjs/src/gen/virtru/policy/certificates/v1/certificates_pb.js +0 -77
  60. package/lib-commonjs/src/gen/virtru/policy/objects_pb.js +0 -143
  61. package/lib-commonjs/src/index.js +0 -48
  62. package/lib-commonjs/src/lib/client.js +0 -66
  63. package/lib-commonjs/src/lib/consts.js +0 -10
  64. package/lib-commonjs/src/lib/dsp.js +0 -49
  65. package/lib-commonjs/src/lib/utils.js +0 -402
  66. package/lib-commonjs/tests/dsp-client.test.js +0 -75
  67. package/lib-commonjs/tests/dsp.test.js +0 -94
  68. package/lib-commonjs/tests/mocks/create-export-artifacts.js +0 -17
  69. package/lib-commonjs/tests/mocks/tagging-pdp-tag.js +0 -15
  70. package/lib-commonjs/tests/mocks/well-known-configuration.js +0 -16
  71. package/lib-commonjs/tests/setup-msw.js +0 -33
  72. package/src/gen/buf/validate/validate_pb.ts +0 -4879
  73. package/src/gen/config/formatters/testdata/v1/test_pb.ts +0 -287
  74. package/src/gen/config/v1/config_connect.ts +0 -111
  75. package/src/gen/config/v1/config_pb.ts +0 -439
  76. package/src/gen/config/v1/kas_config_pb.ts +0 -183
  77. package/src/gen/config/v1/meta_pb.ts +0 -72
  78. package/src/gen/config/v1/outlook_config_pb.ts +0 -242
  79. package/src/gen/config/v1/secureviewer_config_pb.ts +0 -161
  80. package/src/gen/config/v1/tagging_pb.ts +0 -456
  81. package/src/gen/google/api/annotations_pb.ts +0 -43
  82. package/src/gen/google/api/http_pb.ts +0 -486
  83. package/src/gen/helloworld/v1/helloworld_connect.ts +0 -49
  84. package/src/gen/helloworld/v1/helloworld_pb.ts +0 -104
  85. package/src/gen/kas/nanotdf/v1/nanotdf_rewrap_connect.ts +0 -39
  86. package/src/gen/kas/nanotdf/v1/nanotdf_rewrap_pb.ts +0 -207
  87. package/src/gen/policyimportexport/v1/policy_import_export_connect.ts +0 -56
  88. package/src/gen/policyimportexport/v1/policy_import_export_pb.ts +0 -332
  89. package/src/gen/shared/v1/shared_connect.ts +0 -61
  90. package/src/gen/shared/v1/shared_pb.ts +0 -240
  91. package/src/gen/shared/v2/shared_connect.ts +0 -39
  92. package/src/gen/shared/v2/shared_pb.ts +0 -122
  93. package/src/gen/tagging/pdp/v2/externalprocessors/processor_connect.ts +0 -156
  94. package/src/gen/tagging/pdp/v2/externalprocessors/processor_pb.ts +0 -709
  95. package/src/gen/tagging/pdp/v2/stanag/stanag_pb.ts +0 -953
  96. package/src/gen/tagging/pdp/v2/tagging_connect.ts +0 -73
  97. package/src/gen/tagging/pdp/v2/tagging_pb.ts +0 -1064
  98. package/src/gen/version/v1/version_connect.ts +0 -31
  99. package/src/gen/version/v1/version_pb.ts +0 -143
  100. package/src/gen/virtru/common/common_pb.ts +0 -201
  101. package/src/gen/virtru/policy/certificates/v1/certificates_connect.ts +0 -44
  102. package/src/gen/virtru/policy/certificates/v1/certificates_pb.ts +0 -305
  103. package/src/gen/virtru/policy/objects_pb.ts +0 -253
  104. package/src/gen/web-admin/v1/config_connect.ts +0 -52
  105. package/src/gen/web-admin/v1/config_pb.ts +0 -173
  106. package/src/index.ts +0 -62
  107. package/src/lib/client.ts +0 -90
  108. package/src/lib/consts.ts +0 -8
  109. package/src/lib/dsp.ts +0 -65
  110. package/src/lib/utils.test.ts +0 -412
  111. package/src/lib/utils.ts +0 -524
  112. package/src/vite-env.d.ts +0 -7
  113. package/temp/build/lint/_eslint-5eVG3S6w.json +0 -30
  114. package/temp/test/jest/haste-map-b055a9550e6d9f9b43d8ad29b1607278-49d19aee56a0732eca9c014a51272338-8710993d07b75a801b0956e67ffc46fa +0 -0
  115. package/tests/dsp-client.test.ts +0 -95
  116. package/tests/dsp.test.ts +0 -119
  117. package/tests/mocks/create-export-artifacts.ts +0 -16
  118. package/tests/mocks/tagging-pdp-tag.ts +0 -13
  119. package/tests/mocks/well-known-configuration.ts +0 -15
  120. package/tests/setup-msw.ts +0 -35
  121. package/tsconfig.json +0 -32
  122. package/update-protos.sh +0 -63
  123. package/vitest.config.ts +0 -32
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Change Log - @virtru/dsp-sdk
2
2
 
3
- This log was last generated on Tue, 03 Mar 2026 16:24:41 GMT and should not be manually modified.
3
+ This log was last generated on Thu, 12 Mar 2026 20:33:42 GMT and should not be manually modified.
4
+
5
+ ## 0.6.0
6
+ Thu, 12 Mar 2026 20:33:42 GMT
7
+
8
+ ### Minor changes
9
+
10
+ - expose fipsCryptoService implementation, useFips option
11
+
12
+ ## 0.5.0
13
+ Thu, 12 Mar 2026 16:58:37 GMT
14
+
15
+ ### Minor changes
16
+
17
+ - add FipsCryptoService implementation
4
18
 
5
19
  ## 0.4.1
6
20
  Tue, 03 Mar 2026 16:24:41 GMT
package/README.md CHANGED
@@ -79,16 +79,96 @@ console.log(decrypted); // "hello world"
79
79
 
80
80
  ```
81
81
 
82
+ ### FIPS Mode
83
+
84
+ The SDK supports FIPS 140-2 compliant cryptography using a validated BoringSSL WASM module. To enable FIPS mode:
85
+
86
+ ```typescript
87
+ import { AuthProviders, DSP } from '@virtru/dsp-sdk';
88
+ import { FipsCryptoService } from '@virtru/dsp-sdk/fips';
89
+
90
+ // Create auth provider with FIPS crypto service
91
+ const authProvider = await AuthProviders.clientSecretAuthProvider(
92
+ {
93
+ clientId: 'your-client-id',
94
+ clientSecret: 'your-client-secret',
95
+ oidcOrigin: 'https://oidc-endpoint',
96
+ exchange: 'client',
97
+ },
98
+ FipsCryptoService // Use FIPS-validated crypto
99
+ );
100
+
101
+ // Initialize DSP with FIPS mode enabled
102
+ const dsp = new DSP({
103
+ authProvider,
104
+ platformUrl: 'https://dsp-platform-url',
105
+ useFips: true, // Enables FIPS mode
106
+ });
107
+
108
+ // Check if FIPS is enabled
109
+ console.log('FIPS enabled:', dsp.fipsEnabled); // true
110
+ ```
111
+
112
+ When FIPS mode is enabled, TDF3/ZTDF cryptographic operations (encryption, decryption, signing, key generation) use the FIPS-validated BoringSSL WASM module instead of Web Crypto API (`crypto.subtle`). NanoTDF operations are not covered by FIPS mode and continue to use `crypto.subtle`.
113
+
114
+ **Note:** FIPS mode only supports RSA keys; EC (Elliptic Curve) operations are not available in FIPS mode.
115
+
82
116
  Refer to the API documentation for a complete list of available methods and configuration options.
83
117
 
84
118
  ## Testing
85
119
 
86
- This section is intended for repository maintainers and contributors. To verify the integrity of the SDK and ensure all features work as expected, run the test suite using the following command:
120
+ This section is intended for repository maintainers and contributors.
121
+
122
+ The test setup is split into:
123
+ - **Unit tests** (`test:unit`): run in Vitest `jsdom` mode and do **not** require Playwright.
124
+ - **Integration/browser tests** (`test:integration`): run in Chromium via Playwright.
125
+
126
+ To run the unit suite:
127
+
128
+ ```bash
129
+ pnpm run build-fips-crypto
130
+ pnpm run copy-wasm
131
+ pnpm test:unit
132
+ ```
133
+
134
+ To run the browser/integration suite:
135
+
136
+ ```bash
137
+ pnpm test:integration
138
+ ```
139
+
140
+ To run both:
87
141
 
88
142
  ```bash
89
143
  pnpm test
90
144
  ```
91
145
 
146
+ ### Integration Tests
147
+
148
+ Integration tests verify end-to-end encrypt/decrypt operations against a live DSP server, including FIPS mode compliance testing.
149
+
150
+ **Required environment variables:**
151
+ ```bash
152
+ export DSP_PLATFORM_URL="http://localhost:8080" # DSP platform URL
153
+ export CLIENT_ID="your-client-id" # OAuth client ID
154
+ export CLIENT_SECRET="your-client-secret" # OAuth client secret
155
+ export OIDC_ENDPOINT="http://localhost:8888/auth/realms/opentdf" # OIDC endpoint
156
+ ```
157
+
158
+ **Run integration tests:**
159
+ ```bash
160
+ pnpm run build-fips-crypto
161
+ pnpm run copy-wasm
162
+ pnpm test:integration
163
+ ```
164
+
165
+ **Run all tests (unit + integration):**
166
+ ```bash
167
+ pnpm test
168
+ ```
169
+
170
+ Integration tests include FIPS compliance verification to ensure `crypto.subtle` is not used when FIPS mode is enabled (validating that only FIPS-certified BoringSSL WASM is used for cryptographic operations).
171
+
92
172
 
93
173
  ### Linting
94
174
 
@@ -165,5 +245,3 @@ You only need to regenerate the code when:
165
245
  - Proto dependencies are updated
166
246
 
167
247
  After regenerating, commit the updated files in `src/gen/` to version control.
168
-
169
-
@@ -0,0 +1,7 @@
1
+ /**
2
+ * FIPS entrypoint — only import this if @virtru-private/fips-crypto-js is installed.
3
+ *
4
+ * @example
5
+ * import { FipsCryptoService, initializeFipsCrypto } from '@virtru/dsp-sdk/fips';
6
+ */
7
+ export { FipsCryptoService, initializeFipsCrypto, } from './lib/fips-crypto-service';
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Copyright (c) Virtru Corporation. All rights reserved.
3
+ *
4
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
+ */
6
+ /**
7
+ * FIPS entrypoint — only import this if @virtru-private/fips-crypto-js is installed.
8
+ *
9
+ * @example
10
+ * import { FipsCryptoService, initializeFipsCrypto } from '@virtru/dsp-sdk/fips';
11
+ */
12
+ export { FipsCryptoService, initializeFipsCrypto, } from './lib/fips-crypto-service';
@@ -1,4 +1,4 @@
1
- import { OpenTDF, type OpenTDFOptions } from '@opentdf/sdk';
1
+ import { OpenTDF, type OpenTDFOptions, type CryptoService } from '@opentdf/sdk';
2
2
  import { type PlatformServices } from '@opentdf/sdk/platform';
3
3
  import { type DSPClientServicesV1, type DSPClientServicesV2 } from './client';
4
4
  import type { Interceptor } from '@connectrpc/connect';
@@ -7,8 +7,24 @@ export type DSPServicesV2 = DSPClientServicesV2;
7
7
  export interface DSPOptions extends OpenTDFOptions {
8
8
  platformUrl: string;
9
9
  interceptors?: Interceptor[];
10
+ /**
11
+ * Enable FIPS 140-2 compliant cryptography for TDF3/ZTDF operations.
12
+ * When true, uses @virtru-private/fips-mode for encryption/decryption.
13
+ * Requires @virtru-private/fips-crypto-js WASM module to be loaded.
14
+ *
15
+ * Note: NanoTDF operations are NOT affected by this flag and will
16
+ * continue to use native crypto.subtle (not FIPS-compliant).
17
+ */
18
+ useFips?: boolean;
19
+ /**
20
+ * Optional: Provide a custom CryptoService implementation for TDF3/ZTDF.
21
+ * If provided, this takes precedence over useFips flag.
22
+ * NanoTDF operations are not affected.
23
+ */
24
+ cryptoService?: CryptoService;
10
25
  }
11
26
  export declare class DSP extends OpenTDF {
27
+ readonly fipsEnabled: boolean;
12
28
  readonly services: {
13
29
  v1: DSPServicesV1;
14
30
  v2: DSPServicesV2;
@@ -3,21 +3,44 @@
3
3
  *
4
4
  * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
5
  */
6
- import { OpenTDF } from '@opentdf/sdk';
6
+ import { OpenTDF, } from '@opentdf/sdk';
7
7
  import { PlatformClient } from '@opentdf/sdk/platform';
8
8
  import { DSPClient, } from './client';
9
+ import { FipsCryptoService, initializeFipsCrypto } from './fips-crypto-service';
9
10
  export class DSP extends OpenTDF {
10
11
  constructor(options) {
11
- super(options);
12
+ // Validate required options first
13
+ if (!options.platformUrl) {
14
+ throw new Error('platformUrl is required for DSP client');
15
+ }
16
+ // Configure FIPS crypto service for TDF3/ZTDF
17
+ const configuredOptions = { ...options };
18
+ if (options.useFips && options.cryptoService) {
19
+ console.warn('[DSP] Both useFips and cryptoService are set. useFips will be ignored — the provided cryptoService will be used.');
20
+ }
21
+ if (options.cryptoService) {
22
+ configuredOptions.cryptoService = options.cryptoService;
23
+ }
24
+ else if (options.useFips) {
25
+ initializeFipsCrypto();
26
+ configuredOptions.cryptoService = FipsCryptoService;
27
+ }
28
+ // Pass configured options to OpenTDF
29
+ super(configuredOptions);
30
+ Object.defineProperty(this, "fipsEnabled", {
31
+ enumerable: true,
32
+ configurable: true,
33
+ writable: true,
34
+ value: void 0
35
+ });
12
36
  Object.defineProperty(this, "services", {
13
37
  enumerable: true,
14
38
  configurable: true,
15
39
  writable: true,
16
40
  value: void 0
17
41
  });
18
- if (!options.platformUrl) {
19
- throw new Error('platformUrl is required for DSP client');
20
- }
42
+ this.fipsEnabled = configuredOptions.cryptoService === FipsCryptoService;
43
+ // Setup DSP and Platform clients
21
44
  const dspClient = new DSPClient({
22
45
  platformUrl: options.platformUrl,
23
46
  interceptors: options.interceptors,
@@ -0,0 +1,35 @@
1
+ import { Binary } from '@opentdf/sdk/singlecontainer';
2
+ import { type AsymmetricSigningAlgorithm, type ECCurve, type HkdfParams, type KeyPair, type PrivateKey, type PublicKey, type SymmetricKey } from '@opentdf/sdk/singlecontainer';
3
+ /**
4
+ * Generate an RSA key pair for encryption/decryption (RSA-OAEP).
5
+ *
6
+ * VirtruCrypto generates a single combined keypair handle. Both the PublicKey
7
+ * and PrivateKey opaque wrappers reference the same VirtruCryptoKey — the
8
+ * handle already carries all necessary usages. No export/re-import needed.
9
+ */
10
+ export declare function generateKeyPair(_size?: number): Promise<KeyPair>;
11
+ /**
12
+ * Generate an RSA key pair for signing/verification (RSASSA-PKCS1-v1_5).
13
+ */
14
+ export declare function generateSigningKeyPair(): Promise<KeyPair>;
15
+ /**
16
+ * Encrypt with RSA-OAEP public key.
17
+ * Accepts Binary payload or a SymmetricKey for key-wrapping.
18
+ */
19
+ export declare function encryptWithPublicKey(payload: Binary | SymmetricKey, publicKey: PublicKey): Promise<Binary>;
20
+ /**
21
+ * Decrypt with RSA-OAEP private key.
22
+ */
23
+ export declare function decryptWithPrivateKey(encryptedPayload: Binary, privateKey: PrivateKey): Promise<Binary>;
24
+ /**
25
+ * Sign data with an RSA private key (RS256 only).
26
+ * VirtruCrypto requires pre-hashed data, so the data is SHA-256 hashed first.
27
+ */
28
+ export declare function sign(data: Uint8Array, privateKey: PrivateKey, algorithm: AsymmetricSigningAlgorithm): Promise<Uint8Array>;
29
+ /**
30
+ * Verify an RS256 signature.
31
+ * VirtruCrypto requires pre-hashed data.
32
+ */
33
+ export declare function verify(data: Uint8Array, signature: Uint8Array, publicKey: PublicKey, algorithm: AsymmetricSigningAlgorithm): Promise<boolean>;
34
+ export declare function generateECKeyPair(_curve?: ECCurve): Promise<KeyPair>;
35
+ export declare function deriveKeyFromECDH(_privateKey: PrivateKey, _publicKey: PublicKey, _hkdfParams: HkdfParams): Promise<SymmetricKey>;
@@ -0,0 +1,116 @@
1
+ /*
2
+ * Copyright (c) Virtru Corporation. All rights reserved.
3
+ *
4
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
+ */
6
+ import { Binary } from '@opentdf/sdk/singlecontainer';
7
+ import {} from '@opentdf/sdk/singlecontainer';
8
+ import { ConfigurationError } from '@opentdf/sdk';
9
+ import { getVirtruCrypto } from './primitives';
10
+ import { isSymmetricKey, wrapKeyPair, unwrapPublicKey, unwrapPrivateKey, unwrapSymmetricKey } from './keys';
11
+ // ─── Key Generation ───────────────────────────────────────────────────────────
12
+ const ENC_DEC_METHODS = ['encrypt', 'decrypt'];
13
+ const SIGN_VERIFY_METHODS = ['sign', 'verify'];
14
+ const RSA_OAEP_PARAMS = {
15
+ name: 'RSA-OAEP',
16
+ modulusLength: 2048,
17
+ publicExponent: [1, 0, 1],
18
+ hash: 'SHA-256',
19
+ };
20
+ const RS256_PARAMS = {
21
+ name: 'RSASSA-PKCS1-v1_5',
22
+ modulusLength: 2048,
23
+ publicExponent: [1, 0, 1],
24
+ hash: 'SHA-256',
25
+ };
26
+ /**
27
+ * Generate an RSA key pair for encryption/decryption (RSA-OAEP).
28
+ *
29
+ * VirtruCrypto generates a single combined keypair handle. Both the PublicKey
30
+ * and PrivateKey opaque wrappers reference the same VirtruCryptoKey — the
31
+ * handle already carries all necessary usages. No export/re-import needed.
32
+ */
33
+ export async function generateKeyPair(_size) {
34
+ const crypto = await getVirtruCrypto();
35
+ const combined = await crypto.generateKey(RSA_OAEP_PARAMS, true, ENC_DEC_METHODS);
36
+ return wrapKeyPair(combined, combined, 'rsa:2048');
37
+ }
38
+ /**
39
+ * Generate an RSA key pair for signing/verification (RSASSA-PKCS1-v1_5).
40
+ */
41
+ export async function generateSigningKeyPair() {
42
+ const crypto = await getVirtruCrypto();
43
+ const combined = await crypto.generateKey(RS256_PARAMS, true, SIGN_VERIFY_METHODS);
44
+ return wrapKeyPair(combined, combined, 'rsa:2048');
45
+ }
46
+ // ─── RSA Encrypt / Decrypt ────────────────────────────────────────────────────
47
+ /**
48
+ * Encrypt with RSA-OAEP public key.
49
+ * Accepts Binary payload or a SymmetricKey for key-wrapping.
50
+ */
51
+ export async function encryptWithPublicKey(payload, publicKey) {
52
+ const crypto = await getVirtruCrypto();
53
+ let payloadBytes;
54
+ if (isSymmetricKey(payload)) {
55
+ payloadBytes = await crypto.exportKey('raw', unwrapSymmetricKey(payload));
56
+ }
57
+ else {
58
+ payloadBytes = new Uint8Array(payload.asArrayBuffer());
59
+ }
60
+ const vKey = unwrapPublicKey(publicKey);
61
+ const encrypted = await crypto.encrypt(RSA_OAEP_PARAMS, vKey, payloadBytes);
62
+ return Binary.fromArrayBuffer(encrypted.buffer);
63
+ }
64
+ /**
65
+ * Decrypt with RSA-OAEP private key.
66
+ */
67
+ export async function decryptWithPrivateKey(encryptedPayload, privateKey) {
68
+ const crypto = await getVirtruCrypto();
69
+ const vKey = unwrapPrivateKey(privateKey);
70
+ const payloadBytes = new Uint8Array(encryptedPayload.asArrayBuffer());
71
+ const decrypted = await crypto.decrypt(RSA_OAEP_PARAMS, vKey, payloadBytes);
72
+ return Binary.fromArrayBuffer(decrypted.buffer);
73
+ }
74
+ // ─── Sign / Verify ────────────────────────────────────────────────────────────
75
+ /**
76
+ * Sign data with an RSA private key (RS256 only).
77
+ * VirtruCrypto requires pre-hashed data, so the data is SHA-256 hashed first.
78
+ */
79
+ export async function sign(data, privateKey, algorithm) {
80
+ if (algorithm !== 'RS256') {
81
+ throw new ConfigurationError(`Signing algorithm '${algorithm}' not supported in FIPS mode. Only RS256 is supported.`);
82
+ }
83
+ const crypto = await getVirtruCrypto();
84
+ const vKey = unwrapPrivateKey(privateKey);
85
+ // VirtruCrypto signs a pre-computed hash, not raw data
86
+ const hash = await crypto.digest({ name: 'SHA-256' }, data);
87
+ const sig = await crypto.sign(RS256_PARAMS, vKey, new Uint8Array(hash));
88
+ return new Uint8Array(sig);
89
+ }
90
+ /**
91
+ * Verify an RS256 signature.
92
+ * VirtruCrypto requires pre-hashed data.
93
+ */
94
+ export async function verify(data, signature, publicKey, algorithm) {
95
+ if (algorithm !== 'RS256') {
96
+ throw new ConfigurationError(`Verification algorithm '${algorithm}' not supported in FIPS mode. Only RS256 is supported.`);
97
+ }
98
+ const crypto = await getVirtruCrypto();
99
+ const vKey = unwrapPublicKey(publicKey);
100
+ const hash = await crypto.digest({ name: 'SHA-256' }, data);
101
+ try {
102
+ return await crypto.verify(RS256_PARAMS, vKey, signature, new Uint8Array(hash));
103
+ }
104
+ catch (e) {
105
+ if (e === 'Signature verification error')
106
+ return false;
107
+ throw e;
108
+ }
109
+ }
110
+ // ─── EC Stubs (unsupported in FIPS mode) ─────────────────────────────────────
111
+ export async function generateECKeyPair(_curve) {
112
+ throw new ConfigurationError('EC key generation is not supported in FIPS mode.');
113
+ }
114
+ export async function deriveKeyFromECDH(_privateKey, _publicKey, _hkdfParams) {
115
+ throw new ConfigurationError('ECDH key derivation is not supported in FIPS mode.');
116
+ }
@@ -0,0 +1,51 @@
1
+ import { type KeyOptions, type PrivateKey, type PublicKey, type PublicKeyInfo } from '@opentdf/sdk/singlecontainer';
2
+ /**
3
+ * Import a PEM public key (SPKI) as an opaque PublicKey.
4
+ * Only RSA keys are supported in FIPS mode.
5
+ *
6
+ * @param pem - PEM-encoded public key (`-----BEGIN PUBLIC KEY-----`)
7
+ * @param options.usage - `'encrypt'` (RSA-OAEP) or `'sign'` (RS256 verify). Defaults to `'encrypt'`.
8
+ * @param options.extractable - Defaults to `true`.
9
+ * @param options.algorithmHint - Skips SPKI parsing when provided.
10
+ */
11
+ export declare function importPublicKey(pem: string, options: KeyOptions): Promise<PublicKey>;
12
+ /**
13
+ * Import a PEM private key (PKCS#8) as an opaque PrivateKey.
14
+ * Only RSA keys are supported in FIPS mode.
15
+ *
16
+ * @param pem - PEM-encoded private key (`-----BEGIN PRIVATE KEY-----`)
17
+ * @param options.usage - `'encrypt'` (RSA-OAEP decrypt) or `'sign'` (RS256). Defaults to `'encrypt'`.
18
+ * @param options.extractable - Defaults to `false`.
19
+ * @param options.algorithmHint - Algorithm label to attach. Defaults to `'rsa:2048'`.
20
+ */
21
+ export declare function importPrivateKey(pem: string, options: KeyOptions): Promise<PrivateKey>;
22
+ /**
23
+ * Export an opaque public key to PEM (SPKI / `-----BEGIN PUBLIC KEY-----`).
24
+ * Handles VirtruCrypto's PKCS#1 output for generated keys transparently.
25
+ */
26
+ export declare function exportPublicKeyPem(key: PublicKey): Promise<string>;
27
+ /**
28
+ * Export an opaque public key to JWK.
29
+ * Uses WebCrypto for the JWK conversion after normalizing to SPKI.
30
+ */
31
+ export declare function exportPublicKeyJwk(key: PublicKey): Promise<JsonWebKey>;
32
+ /**
33
+ * Export an opaque private key to PEM (PKCS#8 / `-----BEGIN PRIVATE KEY-----`).
34
+ *
35
+ * FOR TESTING / DEVELOPMENT ONLY — not included in the CryptoService interface.
36
+ * Only works for keys that were imported via importPrivateKey (PKCS#8 format).
37
+ * Generated private keys cannot be exported from the FIPS keystore.
38
+ */
39
+ export declare function exportPrivateKeyPem(key: PrivateKey): Promise<string>;
40
+ /**
41
+ * Use the WebCrypto API to parse a PEM public key and extract its algorithm info.
42
+ */
43
+ export declare function parsePublicKeyPem(pem: string): Promise<PublicKeyInfo>;
44
+ /**
45
+ * Use the WebCrypto API to extract the public key PEM from a certificate or PEM string.
46
+ */
47
+ export declare function extractPublicKeyPem(certOrPem: string): Promise<string>;
48
+ /**
49
+ * Use the WebCrypto API to convert a JWK public key to PEM format.
50
+ */
51
+ export declare function jwkToPublicKeyPem(jwk: JsonWebKey): Promise<string>;
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) Virtru Corporation. All rights reserved.
3
+ *
4
+ * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
+ */
6
+ import { WebCryptoService, } from '@opentdf/sdk/singlecontainer';
7
+ import { ConfigurationError } from '@opentdf/sdk';
8
+ import { formatAsPem, removePemFormatting, guessAlgorithmName } from '@opentdf/sdk/cryptoutils';
9
+ import { base64, hex } from '@opentdf/sdk/encodings';
10
+ import { getVirtruCrypto } from './primitives';
11
+ import { wrapPublicKey, wrapPrivateKey, unwrapPublicKey, unwrapPrivateKey } from './keys';
12
+ const VERIFY_USAGE = ['verify'];
13
+ const ENCRYPT_VERIFY_USAGES = ['encrypt', 'verify'];
14
+ const SIGN_USAGE = ['sign'];
15
+ const DECRYPT_USAGE = ['decrypt'];
16
+ // ─── PEM Utilities ────────────────────────────────────────────────────────────
17
+ /**
18
+ * Strip PEM headers/footers and whitespace, decode base64 → raw DER bytes.
19
+ */
20
+ function pemToBytes(pem) {
21
+ return new Uint8Array(base64.decodeArrayBuffer(removePemFormatting(pem)));
22
+ }
23
+ /**
24
+ * Parse the RSA modulus bit length from SPKI-formatted DER bytes.
25
+ * Walks: SEQUENCE { AlgorithmIdentifier, BIT STRING { RSAPublicKey { modulus INTEGER } } }
26
+ */
27
+ function rsaModulusBitsFromSpki(spki) {
28
+ let pos = 0;
29
+ const readLen = () => {
30
+ let len = spki[pos++];
31
+ if (len & 0x80) {
32
+ const n = len & 0x7f;
33
+ len = 0;
34
+ for (let i = 0; i < n; i++)
35
+ len = (len << 8) | spki[pos++];
36
+ }
37
+ return len;
38
+ };
39
+ // SPKI outer SEQUENCE
40
+ pos++;
41
+ readLen();
42
+ // Skip AlgorithmIdentifier SEQUENCE
43
+ pos++;
44
+ pos += readLen();
45
+ // BIT STRING
46
+ pos++;
47
+ readLen();
48
+ pos++; // unused-bits byte
49
+ // RSAPublicKey SEQUENCE
50
+ pos++;
51
+ readLen();
52
+ // modulus INTEGER tag + length
53
+ pos++;
54
+ const modulusLen = readLen();
55
+ // Leading 0x00 is a sign byte for positive INTEGERs — exclude it
56
+ const padding = spki[pos] === 0x00 ? 1 : 0;
57
+ return (modulusLen - padding) * 8;
58
+ }
59
+ // ─── ASN.1 Helpers ────────────────────────────────────────────────────────────
60
+ function asn1EncodeLength(len) {
61
+ if (len < 0x80)
62
+ return new Uint8Array([len]);
63
+ if (len < 0x100)
64
+ return new Uint8Array([0x81, len]);
65
+ return new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
66
+ }
67
+ function asn1Concat(arrays) {
68
+ const total = arrays.reduce((s, a) => s + a.length, 0);
69
+ const out = new Uint8Array(total);
70
+ let off = 0;
71
+ for (const a of arrays) {
72
+ out.set(a, off);
73
+ off += a.length;
74
+ }
75
+ return out;
76
+ }
77
+ function asn1Sequence(elements) {
78
+ const body = asn1Concat(elements);
79
+ return asn1Concat([new Uint8Array([0x30]), asn1EncodeLength(body.length), body]);
80
+ }
81
+ function asn1BitString(data) {
82
+ const inner = asn1Concat([new Uint8Array([0x00]), data]); // 0x00 = zero unused bits
83
+ return asn1Concat([new Uint8Array([0x03]), asn1EncodeLength(inner.length), inner]);
84
+ }
85
+ // RSA AlgorithmIdentifier: SEQUENCE { OID 1.2.840.113549.1.1.1, NULL }
86
+ const RSA_ALG_ID = asn1Sequence([
87
+ new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]),
88
+ new Uint8Array([0x05, 0x00]),
89
+ ]);
90
+ /**
91
+ * Returns true if the DER bytes are already SPKI format (have an AlgorithmIdentifier OID).
92
+ */
93
+ function isSpkiFormat(derBytes) {
94
+ const hexStr = hex.encodeArrayBuffer(derBytes.buffer);
95
+ try {
96
+ guessAlgorithmName(hexStr);
97
+ return true;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Wraps a bare RSAPublicKey (PKCS#1) blob in an SPKI AlgorithmIdentifier container.
105
+ * VirtruCrypto returns PKCS#1 for generated keys; imported keys are already SPKI.
106
+ */
107
+ function normalizePublicKeyToSpki(exported) {
108
+ if (isSpkiFormat(exported))
109
+ return exported;
110
+ return asn1Sequence([RSA_ALG_ID, asn1BitString(exported)]);
111
+ }
112
+ /**
113
+ * Wraps a bare RSAPrivateKey (PKCS#1) blob in a PKCS#8 PrivateKeyInfo container.
114
+ * VirtruCrypto's exportKey('pkcs8') returns raw PKCS#1 DER — the 'pkcs8' flag
115
+ * just selects "export private key" vs "export public key" from the combined slot.
116
+ * We wrap it here to produce standard PKCS#8:
117
+ * SEQUENCE { INTEGER 0, AlgorithmIdentifier, OCTET STRING { RSAPrivateKey } }
118
+ */
119
+ function normalizePrivateKeyToPkcs8(pkcs1) {
120
+ const version = new Uint8Array([0x02, 0x01, 0x00]); // INTEGER 0
121
+ const privateKeyOctetString = asn1Concat([
122
+ new Uint8Array([0x04]),
123
+ asn1EncodeLength(pkcs1.length),
124
+ pkcs1,
125
+ ]);
126
+ return asn1Sequence([version, RSA_ALG_ID, privateKeyOctetString]);
127
+ }
128
+ // ─── Key Import ───────────────────────────────────────────────────────────────
129
+ /**
130
+ * Import a PEM public key (SPKI) as an opaque PublicKey.
131
+ * Only RSA keys are supported in FIPS mode.
132
+ *
133
+ * @param pem - PEM-encoded public key (`-----BEGIN PUBLIC KEY-----`)
134
+ * @param options.usage - `'encrypt'` (RSA-OAEP) or `'sign'` (RS256 verify). Defaults to `'encrypt'`.
135
+ * @param options.extractable - Defaults to `true`.
136
+ * @param options.algorithmHint - Skips SPKI parsing when provided.
137
+ */
138
+ export async function importPublicKey(pem, options) {
139
+ const { usage = 'encrypt', extractable = true, algorithmHint } = options;
140
+ const keyBytes = pemToBytes(pem);
141
+ let algorithm;
142
+ if (algorithmHint) {
143
+ algorithm = algorithmHint;
144
+ }
145
+ else {
146
+ const modulusBits = rsaModulusBitsFromSpki(keyBytes);
147
+ algorithm = modulusBits <= 2048 ? 'rsa:2048' : 'rsa:4096';
148
+ }
149
+ if (!algorithm.startsWith('rsa:')) {
150
+ throw new ConfigurationError(`Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`);
151
+ }
152
+ // VirtruCrypto uses RSASSA-PKCS1-v1_5 as the import algorithm name for all RSA keys;
153
+ // the per-operation algorithm (RSA-OAEP vs PKCS1v15) is specified at call time.
154
+ const usages = usage === 'sign' ? VERIFY_USAGE : ENCRYPT_VERIFY_USAGES;
155
+ const crypto = await getVirtruCrypto();
156
+ const vKey = await crypto.importKey('spki', keyBytes, { name: 'RSASSA-PKCS1-v1_5' }, extractable, usages);
157
+ return wrapPublicKey(vKey, algorithm);
158
+ }
159
+ /**
160
+ * Import a PEM private key (PKCS#8) as an opaque PrivateKey.
161
+ * Only RSA keys are supported in FIPS mode.
162
+ *
163
+ * @param pem - PEM-encoded private key (`-----BEGIN PRIVATE KEY-----`)
164
+ * @param options.usage - `'encrypt'` (RSA-OAEP decrypt) or `'sign'` (RS256). Defaults to `'encrypt'`.
165
+ * @param options.extractable - Defaults to `false`.
166
+ * @param options.algorithmHint - Algorithm label to attach. Defaults to `'rsa:2048'`.
167
+ */
168
+ export async function importPrivateKey(pem, options) {
169
+ const { usage = 'encrypt', extractable = false, algorithmHint } = options;
170
+ const keyBytes = pemToBytes(pem);
171
+ const algorithm = algorithmHint ?? 'rsa:2048';
172
+ if (!algorithm.startsWith('rsa:')) {
173
+ throw new ConfigurationError(`Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`);
174
+ }
175
+ const usages = usage === 'sign' ? SIGN_USAGE : DECRYPT_USAGE;
176
+ const crypto = await getVirtruCrypto();
177
+ const vKey = await crypto.importKey('pkcs8', keyBytes, { name: 'RSASSA-PKCS1-v1_5' }, extractable, usages);
178
+ return wrapPrivateKey(vKey, algorithm);
179
+ }
180
+ // ─── Key Export ───────────────────────────────────────────────────────────────
181
+ /**
182
+ * Export an opaque public key to PEM (SPKI / `-----BEGIN PUBLIC KEY-----`).
183
+ * Handles VirtruCrypto's PKCS#1 output for generated keys transparently.
184
+ */
185
+ export async function exportPublicKeyPem(key) {
186
+ const crypto = await getVirtruCrypto();
187
+ const exported = await crypto.exportKey('spki', unwrapPublicKey(key));
188
+ const spki = normalizePublicKeyToSpki(exported);
189
+ return formatAsPem(spki.buffer, 'PUBLIC KEY');
190
+ }
191
+ /**
192
+ * Export an opaque public key to JWK.
193
+ * Uses WebCrypto for the JWK conversion after normalizing to SPKI.
194
+ */
195
+ export async function exportPublicKeyJwk(key) {
196
+ const crypto = await getVirtruCrypto();
197
+ const exported = await crypto.exportKey('spki', unwrapPublicKey(key));
198
+ const spki = normalizePublicKeyToSpki(exported);
199
+ const webKey = await globalThis.crypto.subtle.importKey('spki', spki, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify']);
200
+ return globalThis.crypto.subtle.exportKey('jwk', webKey);
201
+ }
202
+ /**
203
+ * Export an opaque private key to PEM (PKCS#8 / `-----BEGIN PRIVATE KEY-----`).
204
+ *
205
+ * FOR TESTING / DEVELOPMENT ONLY — not included in the CryptoService interface.
206
+ * Only works for keys that were imported via importPrivateKey (PKCS#8 format).
207
+ * Generated private keys cannot be exported from the FIPS keystore.
208
+ */
209
+ export async function exportPrivateKeyPem(key) {
210
+ const crypto = await getVirtruCrypto();
211
+ const exported = await crypto.exportKey('pkcs8', unwrapPrivateKey(key));
212
+ const pkcs8 = normalizePrivateKeyToPkcs8(exported);
213
+ return formatAsPem(pkcs8.buffer, 'PRIVATE KEY');
214
+ }
215
+ // ─── Public Key Utils ─────────────────────────────────────
216
+ /**
217
+ * Use the WebCrypto API to parse a PEM public key and extract its algorithm info.
218
+ */
219
+ export async function parsePublicKeyPem(pem) {
220
+ return WebCryptoService.parsePublicKeyPem(pem);
221
+ }
222
+ /**
223
+ * Use the WebCrypto API to extract the public key PEM from a certificate or PEM string.
224
+ */
225
+ export async function extractPublicKeyPem(certOrPem) {
226
+ return WebCryptoService.extractPublicKeyPem(certOrPem);
227
+ }
228
+ /**
229
+ * Use the WebCrypto API to convert a JWK public key to PEM format.
230
+ */
231
+ export async function jwkToPublicKeyPem(jwk) {
232
+ return WebCryptoService.jwkToPublicKeyPem(jwk);
233
+ }