@virtru/dsp-sdk 0.4.1 → 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.
- package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_build.chunks.jsonl +2 -2
- package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_test.chunks.jsonl +353 -13
- package/.rush/temp/package-deps__phase_build.json +16 -8
- package/.rush/temp/shrinkwrap-deps.json +99 -94
- package/CHANGELOG.json +21 -0
- package/CHANGELOG.md +8 -1
- package/dist/src/lib/fips-crypto/asymmetric.d.ts +35 -0
- package/dist/src/lib/fips-crypto/asymmetric.js +116 -0
- package/dist/src/lib/fips-crypto/key-format.d.ts +51 -0
- package/dist/src/lib/fips-crypto/key-format.js +233 -0
- package/dist/src/lib/fips-crypto/keys.d.ts +26 -0
- package/dist/src/lib/fips-crypto/keys.js +123 -0
- package/dist/src/lib/fips-crypto/primitives.d.ts +41 -0
- package/dist/src/lib/fips-crypto/primitives.js +105 -0
- package/dist/src/lib/fips-crypto/symmetric.d.ts +61 -0
- package/dist/src/lib/fips-crypto/symmetric.js +152 -0
- package/dist/src/lib/fips-crypto-service.d.ts +5 -0
- package/dist/src/lib/fips-crypto-service.js +44 -0
- package/dist/src/lib/utils.d.ts +8 -2
- package/dist/src/lib/utils.js +8 -9
- package/dist/tests/fips-crypto.unit.test.d.ts +1 -0
- package/dist/tests/fips-crypto.unit.test.js +258 -0
- package/dist/tests/setup-unit.d.ts +7 -0
- package/dist/tests/setup-unit.js +70 -0
- package/lib-commonjs/src/lib/fips-crypto/asymmetric.js +126 -0
- package/lib-commonjs/src/lib/fips-crypto/key-format.js +243 -0
- package/lib-commonjs/src/lib/fips-crypto/keys.js +134 -0
- package/lib-commonjs/src/lib/fips-crypto/primitives.js +112 -0
- package/lib-commonjs/src/lib/fips-crypto/symmetric.js +162 -0
- package/lib-commonjs/src/lib/fips-crypto-service.js +57 -0
- package/lib-commonjs/src/lib/utils.js +8 -9
- package/lib-commonjs/tests/fips-crypto.unit.test.js +260 -0
- package/lib-commonjs/tests/setup-unit.js +72 -0
- package/package.json +10 -5
- package/public/virtru.wasm +0 -0
- package/src/lib/fips-crypto/asymmetric.ts +162 -0
- package/src/lib/fips-crypto/key-format.ts +287 -0
- package/src/lib/fips-crypto/keys.ts +190 -0
- package/src/lib/fips-crypto/primitives.ts +197 -0
- package/src/lib/fips-crypto/symmetric.ts +213 -0
- package/src/lib/fips-crypto-service.ts +58 -0
- package/src/lib/utils.test.ts +11 -8
- package/src/lib/utils.ts +14 -13
- package/temp/build/lint/_eslint-5eVG3S6w.json +9 -1
- package/tests/fips-crypto.unit.test.ts +412 -0
- package/tests/setup-unit.ts +108 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +22 -6
|
@@ -0,0 +1,243 @@
|
|
|
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.importPublicKey = importPublicKey;
|
|
9
|
+
exports.importPrivateKey = importPrivateKey;
|
|
10
|
+
exports.exportPublicKeyPem = exportPublicKeyPem;
|
|
11
|
+
exports.exportPublicKeyJwk = exportPublicKeyJwk;
|
|
12
|
+
exports.exportPrivateKeyPem = exportPrivateKeyPem;
|
|
13
|
+
exports.parsePublicKeyPem = parsePublicKeyPem;
|
|
14
|
+
exports.extractPublicKeyPem = extractPublicKeyPem;
|
|
15
|
+
exports.jwkToPublicKeyPem = jwkToPublicKeyPem;
|
|
16
|
+
const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
|
|
17
|
+
const sdk_1 = require("@opentdf/sdk");
|
|
18
|
+
const cryptoutils_1 = require("@opentdf/sdk/cryptoutils");
|
|
19
|
+
const encodings_1 = require("@opentdf/sdk/encodings");
|
|
20
|
+
const primitives_1 = require("./primitives");
|
|
21
|
+
const keys_1 = require("./keys");
|
|
22
|
+
const VERIFY_USAGE = ['verify'];
|
|
23
|
+
const ENCRYPT_VERIFY_USAGES = ['encrypt', 'verify'];
|
|
24
|
+
const SIGN_USAGE = ['sign'];
|
|
25
|
+
const DECRYPT_USAGE = ['decrypt'];
|
|
26
|
+
// ─── PEM Utilities ────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Strip PEM headers/footers and whitespace, decode base64 → raw DER bytes.
|
|
29
|
+
*/
|
|
30
|
+
function pemToBytes(pem) {
|
|
31
|
+
return new Uint8Array(encodings_1.base64.decodeArrayBuffer((0, cryptoutils_1.removePemFormatting)(pem)));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse the RSA modulus bit length from SPKI-formatted DER bytes.
|
|
35
|
+
* Walks: SEQUENCE { AlgorithmIdentifier, BIT STRING { RSAPublicKey { modulus INTEGER } } }
|
|
36
|
+
*/
|
|
37
|
+
function rsaModulusBitsFromSpki(spki) {
|
|
38
|
+
let pos = 0;
|
|
39
|
+
const readLen = () => {
|
|
40
|
+
let len = spki[pos++];
|
|
41
|
+
if (len & 0x80) {
|
|
42
|
+
const n = len & 0x7f;
|
|
43
|
+
len = 0;
|
|
44
|
+
for (let i = 0; i < n; i++)
|
|
45
|
+
len = (len << 8) | spki[pos++];
|
|
46
|
+
}
|
|
47
|
+
return len;
|
|
48
|
+
};
|
|
49
|
+
// SPKI outer SEQUENCE
|
|
50
|
+
pos++;
|
|
51
|
+
readLen();
|
|
52
|
+
// Skip AlgorithmIdentifier SEQUENCE
|
|
53
|
+
pos++;
|
|
54
|
+
pos += readLen();
|
|
55
|
+
// BIT STRING
|
|
56
|
+
pos++;
|
|
57
|
+
readLen();
|
|
58
|
+
pos++; // unused-bits byte
|
|
59
|
+
// RSAPublicKey SEQUENCE
|
|
60
|
+
pos++;
|
|
61
|
+
readLen();
|
|
62
|
+
// modulus INTEGER tag + length
|
|
63
|
+
pos++;
|
|
64
|
+
const modulusLen = readLen();
|
|
65
|
+
// Leading 0x00 is a sign byte for positive INTEGERs — exclude it
|
|
66
|
+
const padding = spki[pos] === 0x00 ? 1 : 0;
|
|
67
|
+
return (modulusLen - padding) * 8;
|
|
68
|
+
}
|
|
69
|
+
// ─── ASN.1 Helpers ────────────────────────────────────────────────────────────
|
|
70
|
+
function asn1EncodeLength(len) {
|
|
71
|
+
if (len < 0x80)
|
|
72
|
+
return new Uint8Array([len]);
|
|
73
|
+
if (len < 0x100)
|
|
74
|
+
return new Uint8Array([0x81, len]);
|
|
75
|
+
return new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
|
|
76
|
+
}
|
|
77
|
+
function asn1Concat(arrays) {
|
|
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) {
|
|
82
|
+
out.set(a, off);
|
|
83
|
+
off += a.length;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function asn1Sequence(elements) {
|
|
88
|
+
const body = asn1Concat(elements);
|
|
89
|
+
return asn1Concat([new Uint8Array([0x30]), asn1EncodeLength(body.length), body]);
|
|
90
|
+
}
|
|
91
|
+
function asn1BitString(data) {
|
|
92
|
+
const inner = asn1Concat([new Uint8Array([0x00]), data]); // 0x00 = zero unused bits
|
|
93
|
+
return asn1Concat([new Uint8Array([0x03]), asn1EncodeLength(inner.length), inner]);
|
|
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
|
+
* Returns true if the DER bytes are already SPKI format (have an AlgorithmIdentifier OID).
|
|
102
|
+
*/
|
|
103
|
+
function isSpkiFormat(derBytes) {
|
|
104
|
+
const hexStr = encodings_1.hex.encodeArrayBuffer(derBytes.buffer);
|
|
105
|
+
try {
|
|
106
|
+
(0, cryptoutils_1.guessAlgorithmName)(hexStr);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wraps a bare RSAPublicKey (PKCS#1) blob in an SPKI AlgorithmIdentifier container.
|
|
115
|
+
* VirtruCrypto returns PKCS#1 for generated keys; imported keys are already SPKI.
|
|
116
|
+
*/
|
|
117
|
+
function normalizePublicKeyToSpki(exported) {
|
|
118
|
+
if (isSpkiFormat(exported))
|
|
119
|
+
return exported;
|
|
120
|
+
return asn1Sequence([RSA_ALG_ID, asn1BitString(exported)]);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Wraps a bare RSAPrivateKey (PKCS#1) blob in a PKCS#8 PrivateKeyInfo container.
|
|
124
|
+
* VirtruCrypto's exportKey('pkcs8') returns raw PKCS#1 DER — the 'pkcs8' flag
|
|
125
|
+
* just selects "export private key" vs "export public key" from the combined slot.
|
|
126
|
+
* We wrap it here to produce standard PKCS#8:
|
|
127
|
+
* SEQUENCE { INTEGER 0, AlgorithmIdentifier, OCTET STRING { RSAPrivateKey } }
|
|
128
|
+
*/
|
|
129
|
+
function normalizePrivateKeyToPkcs8(pkcs1) {
|
|
130
|
+
const version = new Uint8Array([0x02, 0x01, 0x00]); // INTEGER 0
|
|
131
|
+
const privateKeyOctetString = asn1Concat([
|
|
132
|
+
new Uint8Array([0x04]),
|
|
133
|
+
asn1EncodeLength(pkcs1.length),
|
|
134
|
+
pkcs1,
|
|
135
|
+
]);
|
|
136
|
+
return asn1Sequence([version, RSA_ALG_ID, privateKeyOctetString]);
|
|
137
|
+
}
|
|
138
|
+
// ─── Key Import ───────────────────────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Import a PEM public key (SPKI) as an opaque PublicKey.
|
|
141
|
+
* Only RSA keys are supported in FIPS mode.
|
|
142
|
+
*
|
|
143
|
+
* @param pem - PEM-encoded public key (`-----BEGIN PUBLIC KEY-----`)
|
|
144
|
+
* @param options.usage - `'encrypt'` (RSA-OAEP) or `'sign'` (RS256 verify). Defaults to `'encrypt'`.
|
|
145
|
+
* @param options.extractable - Defaults to `true`.
|
|
146
|
+
* @param options.algorithmHint - Skips SPKI parsing when provided.
|
|
147
|
+
*/
|
|
148
|
+
async function importPublicKey(pem, options) {
|
|
149
|
+
const { usage = 'encrypt', extractable = true, algorithmHint } = options;
|
|
150
|
+
const keyBytes = pemToBytes(pem);
|
|
151
|
+
let algorithm;
|
|
152
|
+
if (algorithmHint) {
|
|
153
|
+
algorithm = algorithmHint;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const modulusBits = rsaModulusBitsFromSpki(keyBytes);
|
|
157
|
+
algorithm = modulusBits <= 2048 ? 'rsa:2048' : 'rsa:4096';
|
|
158
|
+
}
|
|
159
|
+
if (!algorithm.startsWith('rsa:')) {
|
|
160
|
+
throw new sdk_1.ConfigurationError(`Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`);
|
|
161
|
+
}
|
|
162
|
+
// VirtruCrypto uses RSASSA-PKCS1-v1_5 as the import algorithm name for all RSA keys;
|
|
163
|
+
// the per-operation algorithm (RSA-OAEP vs PKCS1v15) is specified at call time.
|
|
164
|
+
const usages = usage === 'sign' ? VERIFY_USAGE : ENCRYPT_VERIFY_USAGES;
|
|
165
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
166
|
+
const vKey = await crypto.importKey('spki', keyBytes, { name: 'RSASSA-PKCS1-v1_5' }, extractable, usages);
|
|
167
|
+
return (0, keys_1.wrapPublicKey)(vKey, algorithm);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Import a PEM private key (PKCS#8) as an opaque PrivateKey.
|
|
171
|
+
* Only RSA keys are supported in FIPS mode.
|
|
172
|
+
*
|
|
173
|
+
* @param pem - PEM-encoded private key (`-----BEGIN PRIVATE KEY-----`)
|
|
174
|
+
* @param options.usage - `'encrypt'` (RSA-OAEP decrypt) or `'sign'` (RS256). Defaults to `'encrypt'`.
|
|
175
|
+
* @param options.extractable - Defaults to `false`.
|
|
176
|
+
* @param options.algorithmHint - Algorithm label to attach. Defaults to `'rsa:2048'`.
|
|
177
|
+
*/
|
|
178
|
+
async function importPrivateKey(pem, options) {
|
|
179
|
+
const { usage = 'encrypt', extractable = false, algorithmHint } = options;
|
|
180
|
+
const keyBytes = pemToBytes(pem);
|
|
181
|
+
const algorithm = algorithmHint ?? 'rsa:2048';
|
|
182
|
+
if (!algorithm.startsWith('rsa:')) {
|
|
183
|
+
throw new sdk_1.ConfigurationError(`Key algorithm '${algorithm}' not supported in FIPS mode. Only RSA keys are supported.`);
|
|
184
|
+
}
|
|
185
|
+
const usages = usage === 'sign' ? SIGN_USAGE : DECRYPT_USAGE;
|
|
186
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
187
|
+
const vKey = await crypto.importKey('pkcs8', keyBytes, { name: 'RSASSA-PKCS1-v1_5' }, extractable, usages);
|
|
188
|
+
return (0, keys_1.wrapPrivateKey)(vKey, algorithm);
|
|
189
|
+
}
|
|
190
|
+
// ─── Key Export ───────────────────────────────────────────────────────────────
|
|
191
|
+
/**
|
|
192
|
+
* Export an opaque public key to PEM (SPKI / `-----BEGIN PUBLIC KEY-----`).
|
|
193
|
+
* Handles VirtruCrypto's PKCS#1 output for generated keys transparently.
|
|
194
|
+
*/
|
|
195
|
+
async function exportPublicKeyPem(key) {
|
|
196
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
197
|
+
const exported = await crypto.exportKey('spki', (0, keys_1.unwrapPublicKey)(key));
|
|
198
|
+
const spki = normalizePublicKeyToSpki(exported);
|
|
199
|
+
return (0, cryptoutils_1.formatAsPem)(spki.buffer, 'PUBLIC KEY');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Export an opaque public key to JWK.
|
|
203
|
+
* Uses WebCrypto for the JWK conversion after normalizing to SPKI.
|
|
204
|
+
*/
|
|
205
|
+
async function exportPublicKeyJwk(key) {
|
|
206
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
207
|
+
const exported = await crypto.exportKey('spki', (0, keys_1.unwrapPublicKey)(key));
|
|
208
|
+
const spki = normalizePublicKeyToSpki(exported);
|
|
209
|
+
const webKey = await globalThis.crypto.subtle.importKey('spki', spki, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify']);
|
|
210
|
+
return globalThis.crypto.subtle.exportKey('jwk', webKey);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Export an opaque private key to PEM (PKCS#8 / `-----BEGIN PRIVATE KEY-----`).
|
|
214
|
+
*
|
|
215
|
+
* FOR TESTING / DEVELOPMENT ONLY — not included in the CryptoService interface.
|
|
216
|
+
* Only works for keys that were imported via importPrivateKey (PKCS#8 format).
|
|
217
|
+
* Generated private keys cannot be exported from the FIPS keystore.
|
|
218
|
+
*/
|
|
219
|
+
async function exportPrivateKeyPem(key) {
|
|
220
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
221
|
+
const exported = await crypto.exportKey('pkcs8', (0, keys_1.unwrapPrivateKey)(key));
|
|
222
|
+
const pkcs8 = normalizePrivateKeyToPkcs8(exported);
|
|
223
|
+
return (0, cryptoutils_1.formatAsPem)(pkcs8.buffer, 'PRIVATE KEY');
|
|
224
|
+
}
|
|
225
|
+
// ─── Public Key Utils ─────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* Use the WebCrypto API to parse a PEM public key and extract its algorithm info.
|
|
228
|
+
*/
|
|
229
|
+
async function parsePublicKeyPem(pem) {
|
|
230
|
+
return singlecontainer_1.WebCryptoService.parsePublicKeyPem(pem);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Use the WebCrypto API to extract the public key PEM from a certificate or PEM string.
|
|
234
|
+
*/
|
|
235
|
+
async function extractPublicKeyPem(certOrPem) {
|
|
236
|
+
return singlecontainer_1.WebCryptoService.extractPublicKeyPem(certOrPem);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Use the WebCrypto API to convert a JWK public key to PEM format.
|
|
240
|
+
*/
|
|
241
|
+
async function jwkToPublicKeyPem(jwk) {
|
|
242
|
+
return singlecontainer_1.WebCryptoService.jwkToPublicKeyPem(jwk);
|
|
243
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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.keyFinalizer = void 0;
|
|
9
|
+
exports.wrapPublicKey = wrapPublicKey;
|
|
10
|
+
exports.wrapPrivateKey = wrapPrivateKey;
|
|
11
|
+
exports.wrapSymmetricKey = wrapSymmetricKey;
|
|
12
|
+
exports.wrapKeyPair = wrapKeyPair;
|
|
13
|
+
exports.unwrapPublicKey = unwrapPublicKey;
|
|
14
|
+
exports.unwrapPrivateKey = unwrapPrivateKey;
|
|
15
|
+
exports.unwrapSymmetricKey = unwrapSymmetricKey;
|
|
16
|
+
exports.isSymmetricKey = isSymmetricKey;
|
|
17
|
+
const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
|
|
18
|
+
// FinalizationRegistry: when an opaque key wrapper is GC'd, release its WASM slot.
|
|
19
|
+
// The held value is any object with a release() method — either a VirtruCryptoKey
|
|
20
|
+
// directly, or a SharedReleasable for key pairs that share a single WASM handle.
|
|
21
|
+
// The registry keeps the held value alive until the callback fires, so release()
|
|
22
|
+
// is always safe to call inside the callback.
|
|
23
|
+
exports.keyFinalizer = new FinalizationRegistry((handle) => {
|
|
24
|
+
handle.release();
|
|
25
|
+
});
|
|
26
|
+
// SharedReleasable wraps a single VirtruCryptoKey handle that is referenced by
|
|
27
|
+
// two wrappers (e.g. the public and private halves of a generated key pair).
|
|
28
|
+
// release() decrements a ref count and only frees the underlying WASM slot
|
|
29
|
+
// when both wrappers have been garbage collected, preventing a double-free.
|
|
30
|
+
function sharedReleasable(key) {
|
|
31
|
+
let refs = 2;
|
|
32
|
+
return {
|
|
33
|
+
release() {
|
|
34
|
+
if (--refs === 0)
|
|
35
|
+
key.release();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Wrap Helpers
|
|
40
|
+
function wrapPublicKey(key, algorithm) {
|
|
41
|
+
const result = {
|
|
42
|
+
_brand: 'PublicKey',
|
|
43
|
+
algorithm,
|
|
44
|
+
_internal: key,
|
|
45
|
+
};
|
|
46
|
+
if (algorithm.startsWith('rsa:')) {
|
|
47
|
+
result.modulusBits = parseInt(algorithm.split(':')[1], 10);
|
|
48
|
+
}
|
|
49
|
+
else if (algorithm.startsWith('ec:')) {
|
|
50
|
+
const curveMap = {
|
|
51
|
+
secp256r1: 'P-256',
|
|
52
|
+
secp384r1: 'P-384',
|
|
53
|
+
secp521r1: 'P-521',
|
|
54
|
+
};
|
|
55
|
+
result.curve = curveMap[algorithm.split(':')[1]];
|
|
56
|
+
}
|
|
57
|
+
exports.keyFinalizer.register(result, key);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function wrapPrivateKey(key, algorithm) {
|
|
61
|
+
const result = {
|
|
62
|
+
_brand: 'PrivateKey',
|
|
63
|
+
algorithm,
|
|
64
|
+
_internal: key,
|
|
65
|
+
};
|
|
66
|
+
if (algorithm.startsWith('rsa:')) {
|
|
67
|
+
result.modulusBits = parseInt(algorithm.split(':')[1], 10);
|
|
68
|
+
}
|
|
69
|
+
exports.keyFinalizer.register(result, key);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function wrapSymmetricKey(key, lengthBits) {
|
|
73
|
+
const result = {
|
|
74
|
+
_brand: 'SymmetricKey',
|
|
75
|
+
length: lengthBits,
|
|
76
|
+
_internal: key,
|
|
77
|
+
};
|
|
78
|
+
exports.keyFinalizer.register(result, key);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function wrapKeyPair(publicKey, privateKey, algorithm) {
|
|
82
|
+
if (publicKey === privateKey) {
|
|
83
|
+
// VirtruCrypto returns a single combined handle for generated key pairs.
|
|
84
|
+
// Use a ref-counted releasable so the WASM slot is freed exactly once
|
|
85
|
+
// after both wrappers are garbage collected.
|
|
86
|
+
const shared = sharedReleasable(publicKey);
|
|
87
|
+
const pub = {
|
|
88
|
+
_brand: 'PublicKey',
|
|
89
|
+
algorithm,
|
|
90
|
+
_internal: publicKey,
|
|
91
|
+
};
|
|
92
|
+
const priv = {
|
|
93
|
+
_brand: 'PrivateKey',
|
|
94
|
+
algorithm,
|
|
95
|
+
_internal: privateKey,
|
|
96
|
+
};
|
|
97
|
+
if (algorithm.startsWith('rsa:')) {
|
|
98
|
+
pub.modulusBits = parseInt(algorithm.split(':')[1], 10);
|
|
99
|
+
priv.modulusBits = pub.modulusBits;
|
|
100
|
+
}
|
|
101
|
+
else if (algorithm.startsWith('ec:')) {
|
|
102
|
+
const curveMap = {
|
|
103
|
+
secp256r1: 'P-256',
|
|
104
|
+
secp384r1: 'P-384',
|
|
105
|
+
secp521r1: 'P-521',
|
|
106
|
+
};
|
|
107
|
+
pub.curve = curveMap[algorithm.split(':')[1]];
|
|
108
|
+
}
|
|
109
|
+
exports.keyFinalizer.register(pub, shared);
|
|
110
|
+
exports.keyFinalizer.register(priv, shared);
|
|
111
|
+
return { publicKey: pub, privateKey: priv };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
publicKey: wrapPublicKey(publicKey, algorithm),
|
|
115
|
+
privateKey: wrapPrivateKey(privateKey, algorithm),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ─── Unwrap Helpers ───────────────────────────────────────────────────────────
|
|
119
|
+
function unwrapPublicKey(key) {
|
|
120
|
+
return key._internal;
|
|
121
|
+
}
|
|
122
|
+
function unwrapPrivateKey(key) {
|
|
123
|
+
return key._internal;
|
|
124
|
+
}
|
|
125
|
+
function unwrapSymmetricKey(key) {
|
|
126
|
+
return key._internal;
|
|
127
|
+
}
|
|
128
|
+
function isSymmetricKey(value) {
|
|
129
|
+
if (!value || typeof value !== 'object' || !('_brand' in value)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const candidate = value;
|
|
133
|
+
return candidate._brand === 'SymmetricKey';
|
|
134
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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.initializeFipsCrypto = exports.getVirtruCrypto = void 0;
|
|
9
|
+
exports.randomBytes = randomBytes;
|
|
10
|
+
exports.digest = digest;
|
|
11
|
+
const sdk_1 = require("@opentdf/sdk");
|
|
12
|
+
const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
|
|
13
|
+
// ─── VirtruCrypto Init ────────────────────────────────────────────────────────
|
|
14
|
+
let virtruCryptoInstance = null;
|
|
15
|
+
let virtruCryptoInitPromise = null;
|
|
16
|
+
// fips-crypto-js initialization is asynchronous (WASM fetch + runtime init).
|
|
17
|
+
// Keep this bounded so callers fail fast with a clear error if readiness never
|
|
18
|
+
// occurs (missing wasm, blocked fetch, or runtime init failure).
|
|
19
|
+
const VIRTRU_CRYPTO_INIT_TIMEOUT_MS = 10000;
|
|
20
|
+
// fips-crypto-js creates window.VirtruFipsWasmLib early, before the WASM
|
|
21
|
+
// runtime finishes wiring methods. Treat it as ready only once initialized.
|
|
22
|
+
function isVirtruCryptoInitialized(candidate) {
|
|
23
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const maybe = candidate;
|
|
27
|
+
return maybe.isInitialized === true && typeof maybe.generateKey === 'function';
|
|
28
|
+
}
|
|
29
|
+
const getVirtruCrypto = async () => {
|
|
30
|
+
if (typeof window === 'undefined') {
|
|
31
|
+
throw new sdk_1.ConfigurationError('FIPS crypto requires a browser environment. VirtruCrypto WASM is not supported in Node.js or SSR contexts.');
|
|
32
|
+
}
|
|
33
|
+
if (virtruCryptoInstance)
|
|
34
|
+
return virtruCryptoInstance;
|
|
35
|
+
if (virtruCryptoInitPromise) {
|
|
36
|
+
await virtruCryptoInitPromise;
|
|
37
|
+
return virtruCryptoInstance;
|
|
38
|
+
}
|
|
39
|
+
virtruCryptoInitPromise = new Promise((resolve, reject) => {
|
|
40
|
+
// Fast path when another import already completed initialization.
|
|
41
|
+
if (isVirtruCryptoInitialized(window.VirtruFipsWasmLib)) {
|
|
42
|
+
resolve();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const onReady = () => {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
const timeout = setTimeout(() => {
|
|
50
|
+
window.removeEventListener('wasmReady', onReady);
|
|
51
|
+
reject(new Error('Timeout waiting for VirtruCrypto WASM initialization.'));
|
|
52
|
+
}, VIRTRU_CRYPTO_INIT_TIMEOUT_MS);
|
|
53
|
+
// fips-crypto-js dispatches wasmReady on window (not document).
|
|
54
|
+
window.addEventListener('wasmReady', onReady, { once: true });
|
|
55
|
+
// Handle race: initialization may complete between the first check and the
|
|
56
|
+
// event listener registration above.
|
|
57
|
+
if (isVirtruCryptoInitialized(window.VirtruFipsWasmLib)) {
|
|
58
|
+
window.removeEventListener('wasmReady', onReady);
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
await virtruCryptoInitPromise;
|
|
64
|
+
virtruCryptoInstance = window.VirtruFipsWasmLib;
|
|
65
|
+
if (!isVirtruCryptoInitialized(virtruCryptoInstance)) {
|
|
66
|
+
throw new Error('VirtruCrypto failed to initialize properly.');
|
|
67
|
+
}
|
|
68
|
+
return virtruCryptoInstance;
|
|
69
|
+
};
|
|
70
|
+
exports.getVirtruCrypto = getVirtruCrypto;
|
|
71
|
+
/**
|
|
72
|
+
* Eagerly initialize FIPS WASM (used by DSP when `useFips: true`).
|
|
73
|
+
* Loaded dynamically so non-FIPS consumers do not require the optional peer.
|
|
74
|
+
*/
|
|
75
|
+
const initializeFipsCrypto = () => {
|
|
76
|
+
// Keep specifier dynamic so Vite/Rollup does not eagerly resolve the optional
|
|
77
|
+
// peer in non-FIPS builds; load occurs only when FIPS mode is enabled.
|
|
78
|
+
const modulePath = '@virtru-private/fips-crypto-js';
|
|
79
|
+
void Promise.resolve(`${modulePath}`).then(s => require(s)).catch((err) => {
|
|
80
|
+
console.error('Virtru FIPS crypto initialization failed: @virtru-private/fips-crypto-js is not installed.', err);
|
|
81
|
+
});
|
|
82
|
+
void (0, exports.getVirtruCrypto)().catch((err) => {
|
|
83
|
+
// Avoid unhandled rejections; same error appears on first crypto call too.
|
|
84
|
+
console.error('Virtru FIPS crypto initialization failed:', err);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
exports.initializeFipsCrypto = initializeFipsCrypto;
|
|
88
|
+
// ─── Randomness / Digest ──────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Generate random bytes via WebCrypto CSPRNG.
|
|
91
|
+
* fips-crypto-js only exposes fixed-length IV generation.
|
|
92
|
+
*
|
|
93
|
+
* Note: this does not route through the fips-crypto-js WASM boundary.
|
|
94
|
+
* It should not be used when a strict "all cryptographic operations must be
|
|
95
|
+
* performed inside the FIPS module" requirement applies.
|
|
96
|
+
*/
|
|
97
|
+
async function randomBytes(byteLength) {
|
|
98
|
+
const buf = new Uint8Array(byteLength);
|
|
99
|
+
globalThis.crypto.getRandomValues(buf);
|
|
100
|
+
return buf;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Compute a hash digest. Only SHA-256 is supported by fips-crypto-js.
|
|
104
|
+
*/
|
|
105
|
+
async function digest(algorithm, data) {
|
|
106
|
+
if (algorithm !== 'SHA-256') {
|
|
107
|
+
throw new sdk_1.ConfigurationError(`Hash algorithm '${algorithm}' not supported in FIPS mode. Only SHA-256 is supported.`);
|
|
108
|
+
}
|
|
109
|
+
const crypto = await (0, exports.getVirtruCrypto)();
|
|
110
|
+
const result = await crypto.digest({ name: 'SHA-256' }, data);
|
|
111
|
+
return new Uint8Array(result);
|
|
112
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.generateKey = generateKey;
|
|
9
|
+
exports.encrypt = encrypt;
|
|
10
|
+
exports.decrypt = decrypt;
|
|
11
|
+
exports.hmac = hmac;
|
|
12
|
+
exports.verifyHmac = verifyHmac;
|
|
13
|
+
exports.importSymmetricKey = importSymmetricKey;
|
|
14
|
+
exports.splitSymmetricKey = splitSymmetricKey;
|
|
15
|
+
exports.mergeSymmetricKeys = mergeSymmetricKeys;
|
|
16
|
+
const singlecontainer_1 = require("@opentdf/sdk/singlecontainer");
|
|
17
|
+
const sdk_1 = require("@opentdf/sdk");
|
|
18
|
+
const primitives_1 = require("./primitives");
|
|
19
|
+
const keys_1 = require("./keys");
|
|
20
|
+
const AES_CBC_URN = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
|
|
21
|
+
const SYM_KEY_METHODS = ['encrypt', 'decrypt', 'sign', 'verify'];
|
|
22
|
+
/**
|
|
23
|
+
* Generate a random AES-256 symmetric key.
|
|
24
|
+
* The VirtruCryptoKey handle is kept alive inside the opaque SymmetricKey —
|
|
25
|
+
* key material never leaves the FIPS WASM keystore.
|
|
26
|
+
*/
|
|
27
|
+
async function generateKey(length) {
|
|
28
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
29
|
+
const bitLen = (length || 32) * 8;
|
|
30
|
+
// extractable=true and internal=true: keystore-based AES key that can be exported
|
|
31
|
+
// for RSA key-wrapping (encryptWithPublicKey calls exportKey on the DEK).
|
|
32
|
+
const vKey = await crypto.generateKey({ name: 'AES-GCM', length: bitLen }, true, SYM_KEY_METHODS, true);
|
|
33
|
+
return (0, keys_1.wrapSymmetricKey)(vKey, bitLen);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt a payload with a symmetric AES key.
|
|
37
|
+
* When payload is a SymmetricKey the key bytes are exported internally to
|
|
38
|
+
* perform key-wrapping — they are never surfaced to the caller.
|
|
39
|
+
*/
|
|
40
|
+
async function encrypt(payload, key, iv, algorithm) {
|
|
41
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
42
|
+
const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
|
|
43
|
+
let payloadBytes;
|
|
44
|
+
if ((0, keys_1.isSymmetricKey)(payload)) {
|
|
45
|
+
// Key-wrapping path: export symmetric key bytes from keystore.
|
|
46
|
+
// Only works if the key was created with extractable:true (e.g. via importSymmetricKey).
|
|
47
|
+
payloadBytes = await crypto.exportKey('raw', (0, keys_1.unwrapSymmetricKey)(payload));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
payloadBytes = new Uint8Array(payload.asArrayBuffer());
|
|
51
|
+
}
|
|
52
|
+
const vKey = (0, keys_1.unwrapSymmetricKey)(key);
|
|
53
|
+
const ivBytes = new Uint8Array(iv.asArrayBuffer());
|
|
54
|
+
const cipherBytes = await crypto.encrypt({ name: algName, length: 256, iv: ivBytes }, vKey, payloadBytes);
|
|
55
|
+
if (algName === 'AES-GCM') {
|
|
56
|
+
return {
|
|
57
|
+
payload: singlecontainer_1.Binary.fromArrayBuffer(cipherBytes.slice(0, -16).buffer),
|
|
58
|
+
authTag: singlecontainer_1.Binary.fromArrayBuffer(cipherBytes.slice(-16).buffer),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { payload: singlecontainer_1.Binary.fromArrayBuffer(cipherBytes.buffer) };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt a payload with a symmetric AES key.
|
|
65
|
+
*/
|
|
66
|
+
async function decrypt(payload, key, iv, algorithm, authTag) {
|
|
67
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
68
|
+
const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
|
|
69
|
+
let payloadBytes = new Uint8Array(payload.asArrayBuffer());
|
|
70
|
+
const ivBytes = new Uint8Array(iv.asArrayBuffer());
|
|
71
|
+
if (algName === 'AES-GCM') {
|
|
72
|
+
if (!authTag) {
|
|
73
|
+
throw new Error('Missing authTag for AES-GCM decryption');
|
|
74
|
+
}
|
|
75
|
+
const tagBytes = new Uint8Array(authTag.asArrayBuffer());
|
|
76
|
+
const combined = new Uint8Array(payloadBytes.length + tagBytes.length);
|
|
77
|
+
combined.set(payloadBytes, 0);
|
|
78
|
+
combined.set(tagBytes, payloadBytes.length);
|
|
79
|
+
payloadBytes = combined;
|
|
80
|
+
}
|
|
81
|
+
const vKey = (0, keys_1.unwrapSymmetricKey)(key);
|
|
82
|
+
const plainBytes = await crypto.decrypt({ name: algName, length: 256, iv: ivBytes }, vKey, payloadBytes);
|
|
83
|
+
return { payload: singlecontainer_1.Binary.fromArrayBuffer(plainBytes.buffer) };
|
|
84
|
+
}
|
|
85
|
+
// ─── HMAC ─────────────────────────────────────────────────────────────────────
|
|
86
|
+
const HMAC_PARAMS = { name: 'HMAC', hash: 'SHA-256' };
|
|
87
|
+
/**
|
|
88
|
+
* Compute HMAC-SHA256.
|
|
89
|
+
* The SymmetricKey must have been created with extractable:true (e.g. via
|
|
90
|
+
* importSymmetricKey) so its bytes can be re-imported as an HMAC key.
|
|
91
|
+
*/
|
|
92
|
+
async function hmac(data, key) {
|
|
93
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
94
|
+
const vKey = (0, keys_1.unwrapSymmetricKey)(key);
|
|
95
|
+
// The key was imported with 'sign' usage (see importSymmetricKey).
|
|
96
|
+
// VirtruCrypto sign() checks usage.includes('sign') then calls the WASM
|
|
97
|
+
// hmac() with the slot number — the key type string is irrelevant for HMAC.
|
|
98
|
+
// Do NOT call release() here: the key handle must remain valid for future calls.
|
|
99
|
+
const sig = await crypto.sign(HMAC_PARAMS, vKey, data);
|
|
100
|
+
return new Uint8Array(sig);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Verify HMAC-SHA256 using constant-time comparison.
|
|
104
|
+
*/
|
|
105
|
+
async function verifyHmac(data, signature, key) {
|
|
106
|
+
const expected = await hmac(data, key);
|
|
107
|
+
if (signature.length !== expected.length)
|
|
108
|
+
return false;
|
|
109
|
+
let mismatch = 0;
|
|
110
|
+
for (let i = 0; i < signature.length; i++) {
|
|
111
|
+
mismatch |= signature[i] ^ expected[i];
|
|
112
|
+
}
|
|
113
|
+
return mismatch === 0;
|
|
114
|
+
}
|
|
115
|
+
// ─── Symmetric Key Import / Split / Merge ─────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* Import raw key bytes as an opaque SymmetricKey.
|
|
118
|
+
* The resulting key is extractable so it can be used for HMAC and key-wrapping.
|
|
119
|
+
*/
|
|
120
|
+
async function importSymmetricKey(keyBytes) {
|
|
121
|
+
const crypto = await (0, primitives_1.getVirtruCrypto)();
|
|
122
|
+
const bitLen = keyBytes.length * 8;
|
|
123
|
+
// Include 'sign'/'verify' so this key can also be used for HMAC-SHA256.
|
|
124
|
+
// VirtruCrypto's sign() checks key.usage.includes('sign'); the underlying
|
|
125
|
+
// WASM hmac() takes the slot number and works with any 256-bit key material.
|
|
126
|
+
const vKey = await crypto.importKey('raw', keyBytes, { name: 'AES-GCM' }, true, SYM_KEY_METHODS);
|
|
127
|
+
return (0, keys_1.wrapSymmetricKey)(vKey, bitLen);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Split a symmetric key into N shares using XOR secret sharing.
|
|
131
|
+
*
|
|
132
|
+
* For numShares === 1 (single-KAS), the key is returned as-is — no key
|
|
133
|
+
* material is exported and no splitting occurs. This is the common DSP case.
|
|
134
|
+
*
|
|
135
|
+
* For numShares > 1 (multi-KAS), XOR secret sharing requires exporting the
|
|
136
|
+
* raw key bytes. This is not yet implemented in FIPS mode.
|
|
137
|
+
*/
|
|
138
|
+
async function splitSymmetricKey(key, numShares) {
|
|
139
|
+
if (numShares === 1) {
|
|
140
|
+
return [key];
|
|
141
|
+
}
|
|
142
|
+
throw new sdk_1.ConfigurationError(`splitSymmetricKey with ${numShares} shares is not yet supported in FIPS mode. ` +
|
|
143
|
+
'Multi-KAS key splitting requires raw key export which violates FIPS key boundary constraints.');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Merge symmetric key shares back into the original key using XOR.
|
|
147
|
+
*
|
|
148
|
+
* For a single share (single-KAS), the share is returned as-is — no key
|
|
149
|
+
* material is exported and no XOR occurs. This is the common DSP case.
|
|
150
|
+
*
|
|
151
|
+
* For multiple shares (multi-KAS), XOR merging requires exporting raw key bytes.
|
|
152
|
+
* This is not yet implemented in FIPS mode.
|
|
153
|
+
*/
|
|
154
|
+
async function mergeSymmetricKeys(shares) {
|
|
155
|
+
if (shares.length === 0)
|
|
156
|
+
throw new sdk_1.ConfigurationError('No shares provided to mergeSymmetricKeys');
|
|
157
|
+
if (shares.length === 1) {
|
|
158
|
+
return shares[0];
|
|
159
|
+
}
|
|
160
|
+
throw new sdk_1.ConfigurationError(`mergeSymmetricKeys with ${shares.length} shares is not yet supported in FIPS mode. ` +
|
|
161
|
+
'Multi-KAS key merging requires raw key export which violates FIPS key boundary constraints.');
|
|
162
|
+
}
|