@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,162 @@
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
+ import { Binary } from '@opentdf/sdk/singlecontainer';
8
+ import {
9
+ type AsymmetricSigningAlgorithm,
10
+ type ECCurve,
11
+ type HkdfParams,
12
+ type KeyPair,
13
+ type PrivateKey,
14
+ type PublicKey,
15
+ type SymmetricKey,
16
+ } from '@opentdf/sdk/singlecontainer';
17
+ import { ConfigurationError } from '@opentdf/sdk';
18
+ import { getVirtruCrypto } from './primitives';
19
+ import { isSymmetricKey, wrapKeyPair, unwrapPublicKey, unwrapPrivateKey, unwrapSymmetricKey } from './keys';
20
+
21
+ // ─── Key Generation ───────────────────────────────────────────────────────────
22
+
23
+ const ENC_DEC_METHODS = ['encrypt', 'decrypt'] as const;
24
+ const SIGN_VERIFY_METHODS = ['sign', 'verify'] as const;
25
+
26
+ const RSA_OAEP_PARAMS = {
27
+ name: 'RSA-OAEP' as const,
28
+ modulusLength: 2048,
29
+ publicExponent: [1, 0, 1],
30
+ hash: 'SHA-256' as const,
31
+ };
32
+
33
+ const RS256_PARAMS = {
34
+ name: 'RSASSA-PKCS1-v1_5' as const,
35
+ modulusLength: 2048,
36
+ publicExponent: [1, 0, 1],
37
+ hash: 'SHA-256' as const,
38
+ };
39
+
40
+ /**
41
+ * Generate an RSA key pair for encryption/decryption (RSA-OAEP).
42
+ *
43
+ * VirtruCrypto generates a single combined keypair handle. Both the PublicKey
44
+ * and PrivateKey opaque wrappers reference the same VirtruCryptoKey — the
45
+ * handle already carries all necessary usages. No export/re-import needed.
46
+ */
47
+ export async function generateKeyPair(_size?: number): Promise<KeyPair> {
48
+ const crypto = await getVirtruCrypto();
49
+ const combined = await crypto.generateKey(RSA_OAEP_PARAMS, true, ENC_DEC_METHODS);
50
+ return wrapKeyPair(combined, combined, 'rsa:2048');
51
+ }
52
+
53
+ /**
54
+ * Generate an RSA key pair for signing/verification (RSASSA-PKCS1-v1_5).
55
+ */
56
+ export async function generateSigningKeyPair(): Promise<KeyPair> {
57
+ const crypto = await getVirtruCrypto();
58
+ const combined = await crypto.generateKey(RS256_PARAMS, true, SIGN_VERIFY_METHODS);
59
+ return wrapKeyPair(combined, combined, 'rsa:2048');
60
+ }
61
+
62
+ // ─── RSA Encrypt / Decrypt ────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Encrypt with RSA-OAEP public key.
66
+ * Accepts Binary payload or a SymmetricKey for key-wrapping.
67
+ */
68
+ export async function encryptWithPublicKey(
69
+ payload: Binary | SymmetricKey,
70
+ publicKey: PublicKey
71
+ ): Promise<Binary> {
72
+ const crypto = await getVirtruCrypto();
73
+
74
+ let payloadBytes: Uint8Array;
75
+ if (isSymmetricKey(payload)) {
76
+ payloadBytes = await crypto.exportKey('raw', unwrapSymmetricKey(payload));
77
+ } else {
78
+ payloadBytes = new Uint8Array(payload.asArrayBuffer());
79
+ }
80
+
81
+ const vKey = unwrapPublicKey(publicKey);
82
+ const encrypted = await crypto.encrypt(RSA_OAEP_PARAMS as any, vKey, payloadBytes);
83
+ return Binary.fromArrayBuffer((encrypted as Uint8Array).buffer as ArrayBuffer);
84
+ }
85
+
86
+ /**
87
+ * Decrypt with RSA-OAEP private key.
88
+ */
89
+ export async function decryptWithPrivateKey(
90
+ encryptedPayload: Binary,
91
+ privateKey: PrivateKey
92
+ ): Promise<Binary> {
93
+ const crypto = await getVirtruCrypto();
94
+ const vKey = unwrapPrivateKey(privateKey);
95
+ const payloadBytes = new Uint8Array(encryptedPayload.asArrayBuffer());
96
+ const decrypted = await crypto.decrypt(RSA_OAEP_PARAMS as any, vKey, payloadBytes);
97
+ return Binary.fromArrayBuffer((decrypted as Uint8Array).buffer as ArrayBuffer);
98
+ }
99
+
100
+ // ─── Sign / Verify ────────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Sign data with an RSA private key (RS256 only).
104
+ * VirtruCrypto requires pre-hashed data, so the data is SHA-256 hashed first.
105
+ */
106
+ export async function sign(
107
+ data: Uint8Array,
108
+ privateKey: PrivateKey,
109
+ algorithm: AsymmetricSigningAlgorithm
110
+ ): Promise<Uint8Array> {
111
+ if (algorithm !== 'RS256') {
112
+ throw new ConfigurationError(
113
+ `Signing algorithm '${algorithm}' not supported in FIPS mode. Only RS256 is supported.`
114
+ );
115
+ }
116
+ const crypto = await getVirtruCrypto();
117
+ const vKey = unwrapPrivateKey(privateKey);
118
+ // VirtruCrypto signs a pre-computed hash, not raw data
119
+ const hash = await crypto.digest({ name: 'SHA-256' }, data);
120
+ const sig = await crypto.sign(RS256_PARAMS, vKey, new Uint8Array(hash));
121
+ return new Uint8Array(sig);
122
+ }
123
+
124
+ /**
125
+ * Verify an RS256 signature.
126
+ * VirtruCrypto requires pre-hashed data.
127
+ */
128
+ export async function verify(
129
+ data: Uint8Array,
130
+ signature: Uint8Array,
131
+ publicKey: PublicKey,
132
+ algorithm: AsymmetricSigningAlgorithm
133
+ ): Promise<boolean> {
134
+ if (algorithm !== 'RS256') {
135
+ throw new ConfigurationError(
136
+ `Verification algorithm '${algorithm}' not supported in FIPS mode. Only RS256 is supported.`
137
+ );
138
+ }
139
+ const crypto = await getVirtruCrypto();
140
+ const vKey = unwrapPublicKey(publicKey);
141
+ const hash = await crypto.digest({ name: 'SHA-256' }, data);
142
+ try {
143
+ return await crypto.verify(RS256_PARAMS, vKey, signature, new Uint8Array(hash));
144
+ } catch (e) {
145
+ if (e === 'Signature verification error') return false;
146
+ throw e;
147
+ }
148
+ }
149
+
150
+ // ─── EC Stubs (unsupported in FIPS mode) ─────────────────────────────────────
151
+
152
+ export async function generateECKeyPair(_curve?: ECCurve): Promise<KeyPair> {
153
+ throw new ConfigurationError('EC key generation is not supported in FIPS mode.');
154
+ }
155
+
156
+ export async function deriveKeyFromECDH(
157
+ _privateKey: PrivateKey,
158
+ _publicKey: PublicKey,
159
+ _hkdfParams: HkdfParams
160
+ ): Promise<SymmetricKey> {
161
+ throw new ConfigurationError('ECDH key derivation is not supported in FIPS mode.');
162
+ }
@@ -0,0 +1,287 @@
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
+ import {
8
+ type KeyAlgorithm,
9
+ type KeyOptions,
10
+ type PrivateKey,
11
+ type PublicKey,
12
+ type PublicKeyInfo,
13
+ WebCryptoService,
14
+ } from '@opentdf/sdk/singlecontainer';
15
+ import { ConfigurationError } from '@opentdf/sdk';
16
+ import { formatAsPem, removePemFormatting, guessAlgorithmName } from '@opentdf/sdk/cryptoutils';
17
+ import { base64, hex } from '@opentdf/sdk/encodings';
18
+ import { getVirtruCrypto, type VirtruKeyUsage } from './primitives';
19
+ import { wrapPublicKey, wrapPrivateKey, unwrapPublicKey, unwrapPrivateKey } from './keys';
20
+
21
+ const VERIFY_USAGE: readonly VirtruKeyUsage[] = ['verify'];
22
+ const ENCRYPT_VERIFY_USAGES: readonly VirtruKeyUsage[] = ['encrypt', 'verify'];
23
+ const SIGN_USAGE: readonly VirtruKeyUsage[] = ['sign'];
24
+ const DECRYPT_USAGE: readonly VirtruKeyUsage[] = ['decrypt'];
25
+
26
+ // ─── PEM Utilities ────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Strip PEM headers/footers and whitespace, decode base64 → raw DER bytes.
30
+ */
31
+ function pemToBytes(pem: string): Uint8Array {
32
+ return new Uint8Array(base64.decodeArrayBuffer(removePemFormatting(pem)));
33
+ }
34
+
35
+ /**
36
+ * Parse the RSA modulus bit length from SPKI-formatted DER bytes.
37
+ * Walks: SEQUENCE { AlgorithmIdentifier, BIT STRING { RSAPublicKey { modulus INTEGER } } }
38
+ */
39
+ function rsaModulusBitsFromSpki(spki: Uint8Array): number {
40
+ let pos = 0;
41
+
42
+ const readLen = (): number => {
43
+ let len = spki[pos++];
44
+ if (len & 0x80) {
45
+ const n = len & 0x7f;
46
+ len = 0;
47
+ for (let i = 0; i < n; i++) len = (len << 8) | spki[pos++];
48
+ }
49
+ return len;
50
+ };
51
+
52
+ // SPKI outer SEQUENCE
53
+ pos++; readLen();
54
+ // Skip AlgorithmIdentifier SEQUENCE
55
+ pos++; pos += readLen();
56
+ // BIT STRING
57
+ pos++; readLen();
58
+ pos++; // unused-bits byte
59
+ // RSAPublicKey SEQUENCE
60
+ pos++; readLen();
61
+ // modulus INTEGER tag + length
62
+ pos++;
63
+ const modulusLen = readLen();
64
+ // Leading 0x00 is a sign byte for positive INTEGERs — exclude it
65
+ const padding = spki[pos] === 0x00 ? 1 : 0;
66
+ return (modulusLen - padding) * 8;
67
+ }
68
+
69
+ // ─── ASN.1 Helpers ────────────────────────────────────────────────────────────
70
+
71
+ function asn1EncodeLength(len: number): Uint8Array {
72
+ if (len < 0x80) return new Uint8Array([len]);
73
+ if (len < 0x100) return new Uint8Array([0x81, len]);
74
+ return new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
75
+ }
76
+
77
+ function asn1Concat(arrays: Uint8Array[]): Uint8Array {
78
+ const total = arrays.reduce((s, a) => s + a.length, 0);
79
+ const out = new Uint8Array(total);
80
+ let off = 0;
81
+ for (const a of arrays) { out.set(a, off); off += a.length; }
82
+ return out;
83
+ }
84
+
85
+ function asn1Sequence(elements: Uint8Array[]): Uint8Array {
86
+ const body = asn1Concat(elements);
87
+ return asn1Concat([new Uint8Array([0x30]), asn1EncodeLength(body.length), body]);
88
+ }
89
+
90
+ function asn1BitString(data: Uint8Array): Uint8Array {
91
+ const inner = asn1Concat([new Uint8Array([0x00]), data]); // 0x00 = zero unused bits
92
+ return asn1Concat([new Uint8Array([0x03]), asn1EncodeLength(inner.length), inner]);
93
+ }
94
+
95
+ // RSA AlgorithmIdentifier: SEQUENCE { OID 1.2.840.113549.1.1.1, NULL }
96
+ const RSA_ALG_ID = asn1Sequence([
97
+ new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]),
98
+ new Uint8Array([0x05, 0x00]),
99
+ ]);
100
+
101
+ /**
102
+ * Returns true if the DER bytes are already SPKI format (have an AlgorithmIdentifier OID).
103
+ */
104
+ function isSpkiFormat(derBytes: Uint8Array): boolean {
105
+ const hexStr = hex.encodeArrayBuffer(derBytes.buffer as ArrayBuffer);
106
+ try {
107
+ guessAlgorithmName(hexStr);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Wraps a bare RSAPublicKey (PKCS#1) blob in an SPKI AlgorithmIdentifier container.
116
+ * VirtruCrypto returns PKCS#1 for generated keys; imported keys are already SPKI.
117
+ */
118
+ function normalizePublicKeyToSpki(exported: Uint8Array): Uint8Array {
119
+ if (isSpkiFormat(exported)) return exported;
120
+ return asn1Sequence([RSA_ALG_ID, asn1BitString(exported)]);
121
+ }
122
+
123
+ /**
124
+ * Wraps a bare RSAPrivateKey (PKCS#1) blob in a PKCS#8 PrivateKeyInfo container.
125
+ * VirtruCrypto's exportKey('pkcs8') returns raw PKCS#1 DER — the 'pkcs8' flag
126
+ * just selects "export private key" vs "export public key" from the combined slot.
127
+ * We wrap it here to produce standard PKCS#8:
128
+ * SEQUENCE { INTEGER 0, AlgorithmIdentifier, OCTET STRING { RSAPrivateKey } }
129
+ */
130
+ function normalizePrivateKeyToPkcs8(pkcs1: Uint8Array): Uint8Array {
131
+ const version = new Uint8Array([0x02, 0x01, 0x00]); // INTEGER 0
132
+ const privateKeyOctetString = asn1Concat([
133
+ new Uint8Array([0x04]),
134
+ asn1EncodeLength(pkcs1.length),
135
+ pkcs1,
136
+ ]);
137
+ return asn1Sequence([version, RSA_ALG_ID, privateKeyOctetString]);
138
+ }
139
+
140
+ // ─── Key Import ───────────────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Import a PEM public key (SPKI) as an opaque PublicKey.
144
+ * Only RSA keys are supported in FIPS mode.
145
+ *
146
+ * @param pem - PEM-encoded public key (`-----BEGIN PUBLIC KEY-----`)
147
+ * @param options.usage - `'encrypt'` (RSA-OAEP) or `'sign'` (RS256 verify). Defaults to `'encrypt'`.
148
+ * @param options.extractable - Defaults to `true`.
149
+ * @param options.algorithmHint - Skips SPKI parsing when provided.
150
+ */
151
+ export async function importPublicKey(pem: string, options: KeyOptions): Promise<PublicKey> {
152
+ const { usage = 'encrypt', extractable = true, algorithmHint } = options;
153
+
154
+ const keyBytes = pemToBytes(pem);
155
+
156
+ let algorithm: KeyAlgorithm;
157
+ if (algorithmHint) {
158
+ algorithm = algorithmHint;
159
+ } else {
160
+ const modulusBits = rsaModulusBitsFromSpki(keyBytes);
161
+ algorithm = modulusBits <= 2048 ? 'rsa:2048' : 'rsa:4096';
162
+ }
163
+
164
+ if (!algorithm.startsWith('rsa:')) {
165
+ throw new ConfigurationError(
166
+ `Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`
167
+ );
168
+ }
169
+
170
+ // VirtruCrypto uses RSASSA-PKCS1-v1_5 as the import algorithm name for all RSA keys;
171
+ // the per-operation algorithm (RSA-OAEP vs PKCS1v15) is specified at call time.
172
+ const usages = usage === 'sign' ? VERIFY_USAGE : ENCRYPT_VERIFY_USAGES;
173
+
174
+ const crypto = await getVirtruCrypto();
175
+ const vKey = await crypto.importKey(
176
+ 'spki',
177
+ keyBytes,
178
+ { name: 'RSASSA-PKCS1-v1_5' },
179
+ extractable,
180
+ usages
181
+ );
182
+
183
+ return wrapPublicKey(vKey, algorithm);
184
+ }
185
+
186
+ /**
187
+ * Import a PEM private key (PKCS#8) as an opaque PrivateKey.
188
+ * Only RSA keys are supported in FIPS mode.
189
+ *
190
+ * @param pem - PEM-encoded private key (`-----BEGIN PRIVATE KEY-----`)
191
+ * @param options.usage - `'encrypt'` (RSA-OAEP decrypt) or `'sign'` (RS256). Defaults to `'encrypt'`.
192
+ * @param options.extractable - Defaults to `false`.
193
+ * @param options.algorithmHint - Algorithm label to attach. Defaults to `'rsa:2048'`.
194
+ */
195
+ export async function importPrivateKey(pem: string, options: KeyOptions): Promise<PrivateKey> {
196
+ const { usage = 'encrypt', extractable = false, algorithmHint } = options;
197
+
198
+ const keyBytes = pemToBytes(pem);
199
+ const algorithm: KeyAlgorithm = algorithmHint ?? 'rsa:2048';
200
+
201
+ if (!algorithm.startsWith('rsa:')) {
202
+ throw new ConfigurationError(
203
+ `Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`
204
+ );
205
+ }
206
+
207
+ const usages = usage === 'sign' ? SIGN_USAGE : DECRYPT_USAGE;
208
+
209
+ const crypto = await getVirtruCrypto();
210
+ const vKey = await crypto.importKey(
211
+ 'pkcs8',
212
+ keyBytes,
213
+ { name: 'RSASSA-PKCS1-v1_5' },
214
+ extractable,
215
+ usages
216
+ );
217
+
218
+ return wrapPrivateKey(vKey, algorithm);
219
+ }
220
+
221
+ // ─── Key Export ───────────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Export an opaque public key to PEM (SPKI / `-----BEGIN PUBLIC KEY-----`).
225
+ * Handles VirtruCrypto's PKCS#1 output for generated keys transparently.
226
+ */
227
+ export async function exportPublicKeyPem(key: PublicKey): Promise<string> {
228
+ const crypto = await getVirtruCrypto();
229
+ const exported = await crypto.exportKey('spki', unwrapPublicKey(key));
230
+ const spki = normalizePublicKeyToSpki(exported);
231
+ return formatAsPem(spki.buffer as ArrayBuffer, 'PUBLIC KEY');
232
+ }
233
+
234
+ /**
235
+ * Export an opaque public key to JWK.
236
+ * Uses WebCrypto for the JWK conversion after normalizing to SPKI.
237
+ */
238
+ export async function exportPublicKeyJwk(key: PublicKey): Promise<JsonWebKey> {
239
+ const crypto = await getVirtruCrypto();
240
+ const exported = await crypto.exportKey('spki', unwrapPublicKey(key));
241
+ const spki = normalizePublicKeyToSpki(exported);
242
+ const webKey = await globalThis.crypto.subtle.importKey(
243
+ 'spki',
244
+ spki,
245
+ { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
246
+ true,
247
+ ['verify']
248
+ );
249
+ return globalThis.crypto.subtle.exportKey('jwk', webKey);
250
+ }
251
+
252
+ /**
253
+ * Export an opaque private key to PEM (PKCS#8 / `-----BEGIN PRIVATE KEY-----`).
254
+ *
255
+ * FOR TESTING / DEVELOPMENT ONLY — not included in the CryptoService interface.
256
+ * Only works for keys that were imported via importPrivateKey (PKCS#8 format).
257
+ * Generated private keys cannot be exported from the FIPS keystore.
258
+ */
259
+ export async function exportPrivateKeyPem(key: PrivateKey): Promise<string> {
260
+ const crypto = await getVirtruCrypto();
261
+ const exported = await crypto.exportKey('pkcs8', unwrapPrivateKey(key));
262
+ const pkcs8 = normalizePrivateKeyToPkcs8(exported);
263
+ return formatAsPem(pkcs8.buffer as ArrayBuffer, 'PRIVATE KEY');
264
+ }
265
+
266
+ // ─── Public Key Utils ─────────────────────────────────────
267
+
268
+ /**
269
+ * Use the WebCrypto API to parse a PEM public key and extract its algorithm info.
270
+ */
271
+ export async function parsePublicKeyPem (pem: string): Promise<PublicKeyInfo> {
272
+ return WebCryptoService.parsePublicKeyPem(pem);
273
+ }
274
+
275
+ /**
276
+ * Use the WebCrypto API to extract the public key PEM from a certificate or PEM string.
277
+ */
278
+ export async function extractPublicKeyPem(certOrPem: string): Promise<string> {
279
+ return WebCryptoService.extractPublicKeyPem(certOrPem);
280
+ }
281
+
282
+ /**
283
+ * Use the WebCrypto API to convert a JWK public key to PEM format.
284
+ */
285
+ export async function jwkToPublicKeyPem(jwk: JsonWebKey): Promise<string> {
286
+ return WebCryptoService.jwkToPublicKeyPem(jwk);
287
+ }
@@ -0,0 +1,190 @@
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
+ import {
8
+ type KeyAlgorithm,
9
+ type KeyPair,
10
+ type PrivateKey,
11
+ type PublicKey,
12
+ type SymmetricKey,
13
+ type ECCurve,
14
+ } from '@opentdf/sdk/singlecontainer';
15
+
16
+ interface VirtruCryptoKeyHandle {
17
+ release(): void;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ // Opaque Key Types
22
+ //
23
+ // Asymmetric keys (PublicKey, PrivateKey) store a VirtruCryptoKey handle,
24
+ // keeping the key material inside the FIPS WASM keystore at all times.
25
+ //
26
+ // Symmetric keys (SymmetricKey) store a VirtruCryptoKey handle pointing to
27
+ // either a keystore slot (imported keys) or a heap allocation (generated keys).
28
+ //
29
+ // All opaque wrappers are registered with keyFinalizer so WASM slots/heap
30
+ // memory are freed automatically when the JS wrapper is garbage collected.
31
+ // This covers the common case where callers (e.g. the opentdf SDK) never
32
+ // call release() explicitly — without this, every TDF operation would
33
+ // permanently consume a keystore slot and the WASM would exhaust all 50 slots (NUM_KEYS).
34
+
35
+ export type OpaquePublicKey = PublicKey & { readonly _internal: VirtruCryptoKeyHandle };
36
+ export type OpaquePrivateKey = PrivateKey & { readonly _internal: VirtruCryptoKeyHandle };
37
+ export type OpaqueSymmetricKey = SymmetricKey & { readonly _internal: VirtruCryptoKeyHandle };
38
+
39
+ type KeyBrand = 'PublicKey' | 'PrivateKey' | 'SymmetricKey';
40
+
41
+ interface OpaqueKeyShapeBase<TBrand extends KeyBrand> {
42
+ _brand: TBrand;
43
+ _internal: VirtruCryptoKeyHandle;
44
+ }
45
+
46
+ interface OpaquePublicKeyShape extends OpaqueKeyShapeBase<'PublicKey'> {
47
+ algorithm: KeyAlgorithm;
48
+ modulusBits?: number;
49
+ curve?: ECCurve;
50
+ }
51
+
52
+ interface OpaquePrivateKeyShape extends OpaqueKeyShapeBase<'PrivateKey'> {
53
+ algorithm: KeyAlgorithm;
54
+ modulusBits?: number;
55
+ }
56
+
57
+ interface OpaqueSymmetricKeyShape extends OpaqueKeyShapeBase<'SymmetricKey'> {
58
+ length: number;
59
+ }
60
+
61
+ // FinalizationRegistry: when an opaque key wrapper is GC'd, release its WASM slot.
62
+ // The held value is any object with a release() method — either a VirtruCryptoKey
63
+ // directly, or a SharedReleasable for key pairs that share a single WASM handle.
64
+ // The registry keeps the held value alive until the callback fires, so release()
65
+ // is always safe to call inside the callback.
66
+ export const keyFinalizer = new FinalizationRegistry((handle: { release(): void }) => {
67
+ handle.release();
68
+ });
69
+
70
+ // SharedReleasable wraps a single VirtruCryptoKey handle that is referenced by
71
+ // two wrappers (e.g. the public and private halves of a generated key pair).
72
+ // release() decrements a ref count and only frees the underlying WASM slot
73
+ // when both wrappers have been garbage collected, preventing a double-free.
74
+ function sharedReleasable(key: VirtruCryptoKeyHandle): { release(): void } {
75
+ let refs = 2;
76
+ return {
77
+ release() {
78
+ if (--refs === 0) key.release();
79
+ },
80
+ };
81
+ }
82
+
83
+ // Wrap Helpers
84
+
85
+ export function wrapPublicKey(key: VirtruCryptoKeyHandle, algorithm: KeyAlgorithm): PublicKey {
86
+ const result: OpaquePublicKeyShape = {
87
+ _brand: 'PublicKey' as const,
88
+ algorithm,
89
+ _internal: key,
90
+ };
91
+ if (algorithm.startsWith('rsa:')) {
92
+ result.modulusBits = parseInt(algorithm.split(':')[1], 10);
93
+ } else if (algorithm.startsWith('ec:')) {
94
+ const curveMap: Record<string, ECCurve> = {
95
+ secp256r1: 'P-256',
96
+ secp384r1: 'P-384',
97
+ secp521r1: 'P-521',
98
+ };
99
+ result.curve = curveMap[algorithm.split(':')[1]];
100
+ }
101
+ keyFinalizer.register(result, key);
102
+ return result as PublicKey;
103
+ }
104
+
105
+ export function wrapPrivateKey(key: VirtruCryptoKeyHandle, algorithm: KeyAlgorithm): PrivateKey {
106
+ const result: OpaquePrivateKeyShape = {
107
+ _brand: 'PrivateKey' as const,
108
+ algorithm,
109
+ _internal: key,
110
+ };
111
+ if (algorithm.startsWith('rsa:')) {
112
+ result.modulusBits = parseInt(algorithm.split(':')[1], 10);
113
+ }
114
+ keyFinalizer.register(result, key);
115
+ return result as PrivateKey;
116
+ }
117
+
118
+ export function wrapSymmetricKey(key: VirtruCryptoKeyHandle, lengthBits: number): SymmetricKey {
119
+ const result: OpaqueSymmetricKeyShape = {
120
+ _brand: 'SymmetricKey' as const,
121
+ length: lengthBits,
122
+ _internal: key,
123
+ };
124
+ keyFinalizer.register(result, key);
125
+ return result as SymmetricKey;
126
+ }
127
+
128
+ export function wrapKeyPair(
129
+ publicKey: VirtruCryptoKeyHandle,
130
+ privateKey: VirtruCryptoKeyHandle,
131
+ algorithm: KeyAlgorithm
132
+ ): KeyPair {
133
+ if (publicKey === privateKey) {
134
+ // VirtruCrypto returns a single combined handle for generated key pairs.
135
+ // Use a ref-counted releasable so the WASM slot is freed exactly once
136
+ // after both wrappers are garbage collected.
137
+ const shared = sharedReleasable(publicKey);
138
+ const pub: OpaquePublicKeyShape = {
139
+ _brand: 'PublicKey' as const,
140
+ algorithm,
141
+ _internal: publicKey,
142
+ };
143
+ const priv: OpaquePrivateKeyShape = {
144
+ _brand: 'PrivateKey' as const,
145
+ algorithm,
146
+ _internal: privateKey,
147
+ };
148
+ if (algorithm.startsWith('rsa:')) {
149
+ pub.modulusBits = parseInt(algorithm.split(':')[1], 10);
150
+ priv.modulusBits = pub.modulusBits;
151
+ } else if (algorithm.startsWith('ec:')) {
152
+ const curveMap: Record<string, ECCurve> = {
153
+ secp256r1: 'P-256',
154
+ secp384r1: 'P-384',
155
+ secp521r1: 'P-521',
156
+ };
157
+ pub.curve = curveMap[algorithm.split(':')[1]];
158
+ }
159
+ keyFinalizer.register(pub, shared);
160
+ keyFinalizer.register(priv, shared);
161
+ return { publicKey: pub as PublicKey, privateKey: priv as PrivateKey };
162
+ }
163
+ return {
164
+ publicKey: wrapPublicKey(publicKey, algorithm),
165
+ privateKey: wrapPrivateKey(privateKey, algorithm),
166
+ };
167
+ }
168
+
169
+ // ─── Unwrap Helpers ───────────────────────────────────────────────────────────
170
+
171
+ export function unwrapPublicKey(key: PublicKey): VirtruCryptoKeyHandle {
172
+ return (key as OpaquePublicKey)._internal;
173
+ }
174
+
175
+ export function unwrapPrivateKey(key: PrivateKey): VirtruCryptoKeyHandle {
176
+ return (key as OpaquePrivateKey)._internal;
177
+ }
178
+
179
+ export function unwrapSymmetricKey(key: SymmetricKey): VirtruCryptoKeyHandle {
180
+ return (key as OpaqueSymmetricKey)._internal;
181
+ }
182
+
183
+ export function isSymmetricKey(value: unknown): value is SymmetricKey {
184
+ if (!value || typeof value !== 'object' || !('_brand' in value)) {
185
+ return false;
186
+ }
187
+
188
+ const candidate = value as { _brand?: unknown };
189
+ return candidate._brand === 'SymmetricKey';
190
+ }