@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.
- package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_build.chunks.jsonl +4 -2
- package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_test.chunks.jsonl +355 -13
- package/.rush/temp/package-deps__phase_build.json +17 -9
- package/.rush/temp/shrinkwrap-deps.json +148 -150
- package/CHANGELOG.json +42 -0
- package/CHANGELOG.md +15 -1
- package/README.md +14 -8
- 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 +12 -7
- 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,197 @@
|
|
|
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 { ConfigurationError } from '@opentdf/sdk';
|
|
8
|
+
import {
|
|
9
|
+
type HashAlgorithm,
|
|
10
|
+
} from '@opentdf/sdk/singlecontainer';
|
|
11
|
+
|
|
12
|
+
// Structural subset of fips-crypto-js. We keep this local to avoid importing
|
|
13
|
+
// optional-peer types in non-FIPS bundles.
|
|
14
|
+
type VirtruCryptoAlgorithm = {
|
|
15
|
+
name: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type VirtruCryptoKeyLike = {
|
|
20
|
+
release: () => void;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type VirtruKeyUsage = 'encrypt' | 'decrypt' | 'sign' | 'verify';
|
|
25
|
+
|
|
26
|
+
type VirtruCryptoLike = {
|
|
27
|
+
isInitialized?: boolean;
|
|
28
|
+
generateKey: (
|
|
29
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
30
|
+
extractable: boolean,
|
|
31
|
+
usages: readonly VirtruKeyUsage[],
|
|
32
|
+
internal?: boolean
|
|
33
|
+
) => Promise<VirtruCryptoKeyLike>;
|
|
34
|
+
digest: (
|
|
35
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
36
|
+
data: Uint8Array
|
|
37
|
+
) => Promise<Uint8Array | ArrayBuffer>;
|
|
38
|
+
encrypt: (
|
|
39
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
40
|
+
key: VirtruCryptoKeyLike,
|
|
41
|
+
data: Uint8Array
|
|
42
|
+
) => Promise<Uint8Array>;
|
|
43
|
+
decrypt: (
|
|
44
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
45
|
+
key: VirtruCryptoKeyLike,
|
|
46
|
+
data: Uint8Array
|
|
47
|
+
) => Promise<Uint8Array>;
|
|
48
|
+
sign: (
|
|
49
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
50
|
+
key: VirtruCryptoKeyLike,
|
|
51
|
+
data: Uint8Array
|
|
52
|
+
) => Promise<Uint8Array>;
|
|
53
|
+
verify: (
|
|
54
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
55
|
+
key: VirtruCryptoKeyLike,
|
|
56
|
+
signature: Uint8Array,
|
|
57
|
+
data: Uint8Array
|
|
58
|
+
) => Promise<boolean>;
|
|
59
|
+
importKey: (
|
|
60
|
+
format: 'raw' | 'spki' | 'pkcs8',
|
|
61
|
+
keyData: Uint8Array,
|
|
62
|
+
algorithm: VirtruCryptoAlgorithm,
|
|
63
|
+
extractable: boolean,
|
|
64
|
+
usages: readonly VirtruKeyUsage[]
|
|
65
|
+
) => Promise<VirtruCryptoKeyLike>;
|
|
66
|
+
exportKey: (
|
|
67
|
+
format: 'raw' | 'spki' | 'pkcs8',
|
|
68
|
+
key: VirtruCryptoKeyLike
|
|
69
|
+
) => Promise<Uint8Array>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ─── VirtruCrypto Init ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
let virtruCryptoInstance: VirtruCryptoLike | null = null;
|
|
75
|
+
let virtruCryptoInitPromise: Promise<void> | null = null;
|
|
76
|
+
|
|
77
|
+
// fips-crypto-js initialization is asynchronous (WASM fetch + runtime init).
|
|
78
|
+
// Keep this bounded so callers fail fast with a clear error if readiness never
|
|
79
|
+
// occurs (missing wasm, blocked fetch, or runtime init failure).
|
|
80
|
+
const VIRTRU_CRYPTO_INIT_TIMEOUT_MS = 10_000;
|
|
81
|
+
|
|
82
|
+
// fips-crypto-js creates window.VirtruFipsWasmLib early, before the WASM
|
|
83
|
+
// runtime finishes wiring methods. Treat it as ready only once initialized.
|
|
84
|
+
function isVirtruCryptoInitialized(candidate: unknown): candidate is VirtruCryptoLike {
|
|
85
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const maybe = candidate as {
|
|
90
|
+
isInitialized?: unknown;
|
|
91
|
+
generateKey?: unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return maybe.isInitialized === true && typeof maybe.generateKey === 'function';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const getVirtruCrypto = async (): Promise<VirtruCryptoLike> => {
|
|
98
|
+
if (typeof window === 'undefined') {
|
|
99
|
+
throw new ConfigurationError(
|
|
100
|
+
'FIPS crypto requires a browser environment. VirtruCrypto WASM is not supported in Node.js or SSR contexts.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (virtruCryptoInstance) return virtruCryptoInstance;
|
|
105
|
+
|
|
106
|
+
if (virtruCryptoInitPromise) {
|
|
107
|
+
await virtruCryptoInitPromise;
|
|
108
|
+
return virtruCryptoInstance!;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
virtruCryptoInitPromise = new Promise<void>((resolve, reject) => {
|
|
112
|
+
// Fast path when another import already completed initialization.
|
|
113
|
+
if (isVirtruCryptoInitialized((window as any).VirtruFipsWasmLib)) {
|
|
114
|
+
resolve();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const onReady = () => {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
resolve();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const timeout = setTimeout(
|
|
124
|
+
() => {
|
|
125
|
+
window.removeEventListener('wasmReady', onReady);
|
|
126
|
+
reject(new Error('Timeout waiting for VirtruCrypto WASM initialization.'));
|
|
127
|
+
},
|
|
128
|
+
VIRTRU_CRYPTO_INIT_TIMEOUT_MS
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// fips-crypto-js dispatches wasmReady on window (not document).
|
|
132
|
+
window.addEventListener('wasmReady', onReady, { once: true });
|
|
133
|
+
|
|
134
|
+
// Handle race: initialization may complete between the first check and the
|
|
135
|
+
// event listener registration above.
|
|
136
|
+
if (isVirtruCryptoInitialized((window as any).VirtruFipsWasmLib)) {
|
|
137
|
+
window.removeEventListener('wasmReady', onReady);
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await virtruCryptoInitPromise;
|
|
144
|
+
|
|
145
|
+
virtruCryptoInstance = (window as any).VirtruFipsWasmLib;
|
|
146
|
+
if (!isVirtruCryptoInitialized(virtruCryptoInstance)) {
|
|
147
|
+
throw new Error('VirtruCrypto failed to initialize properly.');
|
|
148
|
+
}
|
|
149
|
+
return virtruCryptoInstance;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Eagerly initialize FIPS WASM (used by DSP when `useFips: true`).
|
|
154
|
+
* Loaded dynamically so non-FIPS consumers do not require the optional peer.
|
|
155
|
+
*/
|
|
156
|
+
export const initializeFipsCrypto = (): void => {
|
|
157
|
+
// Keep specifier dynamic so Vite/Rollup does not eagerly resolve the optional
|
|
158
|
+
// peer in non-FIPS builds; load occurs only when FIPS mode is enabled.
|
|
159
|
+
const modulePath = '@virtru-private/fips-crypto-js';
|
|
160
|
+
void import(/* @vite-ignore */ modulePath).catch((err) => {
|
|
161
|
+
console.error('Virtru FIPS crypto initialization failed: @virtru-private/fips-crypto-js is not installed.', err);
|
|
162
|
+
});
|
|
163
|
+
void getVirtruCrypto().catch((err) => {
|
|
164
|
+
// Avoid unhandled rejections; same error appears on first crypto call too.
|
|
165
|
+
console.error('Virtru FIPS crypto initialization failed:', err);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ─── Randomness / Digest ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate random bytes via WebCrypto CSPRNG.
|
|
173
|
+
* fips-crypto-js only exposes fixed-length IV generation.
|
|
174
|
+
*
|
|
175
|
+
* Note: this does not route through the fips-crypto-js WASM boundary.
|
|
176
|
+
* It should not be used when a strict "all cryptographic operations must be
|
|
177
|
+
* performed inside the FIPS module" requirement applies.
|
|
178
|
+
*/
|
|
179
|
+
export async function randomBytes(byteLength: number): Promise<Uint8Array> {
|
|
180
|
+
const buf = new Uint8Array(byteLength);
|
|
181
|
+
globalThis.crypto.getRandomValues(buf);
|
|
182
|
+
return buf;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compute a hash digest. Only SHA-256 is supported by fips-crypto-js.
|
|
187
|
+
*/
|
|
188
|
+
export async function digest(algorithm: HashAlgorithm, data: Uint8Array): Promise<Uint8Array> {
|
|
189
|
+
if (algorithm !== 'SHA-256') {
|
|
190
|
+
throw new ConfigurationError(
|
|
191
|
+
`Hash algorithm '${algorithm}' not supported in FIPS mode. Only SHA-256 is supported.`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const crypto = await getVirtruCrypto();
|
|
195
|
+
const result = await crypto.digest({ name: 'SHA-256' }, data);
|
|
196
|
+
return new Uint8Array(result);
|
|
197
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
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, type SymmetricKey } from '@opentdf/sdk/singlecontainer';
|
|
8
|
+
import { ConfigurationError } from '@opentdf/sdk';
|
|
9
|
+
import { getVirtruCrypto } from './primitives';
|
|
10
|
+
import { isSymmetricKey, wrapSymmetricKey, unwrapSymmetricKey } from './keys';
|
|
11
|
+
|
|
12
|
+
type AlgorithmUrn = string;
|
|
13
|
+
type EncryptResult = { payload: Binary; authTag?: Binary };
|
|
14
|
+
type DecryptResult = { payload: Binary };
|
|
15
|
+
|
|
16
|
+
const AES_CBC_URN = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
|
|
17
|
+
|
|
18
|
+
const SYM_KEY_METHODS = ['encrypt', 'decrypt', 'sign', 'verify'] as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a random AES-256 symmetric key.
|
|
22
|
+
* The VirtruCryptoKey handle is kept alive inside the opaque SymmetricKey —
|
|
23
|
+
* key material never leaves the FIPS WASM keystore.
|
|
24
|
+
*/
|
|
25
|
+
export async function generateKey(length?: number): Promise<SymmetricKey> {
|
|
26
|
+
const crypto = await getVirtruCrypto();
|
|
27
|
+
const bitLen = (length || 32) * 8;
|
|
28
|
+
// extractable=true and internal=true: keystore-based AES key that can be exported
|
|
29
|
+
// for RSA key-wrapping (encryptWithPublicKey calls exportKey on the DEK).
|
|
30
|
+
const vKey = await crypto.generateKey(
|
|
31
|
+
{ name: 'AES-GCM', length: bitLen as 256 },
|
|
32
|
+
true,
|
|
33
|
+
SYM_KEY_METHODS,
|
|
34
|
+
true
|
|
35
|
+
);
|
|
36
|
+
return wrapSymmetricKey(vKey, bitLen);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Encrypt a payload with a symmetric AES key.
|
|
41
|
+
* When payload is a SymmetricKey the key bytes are exported internally to
|
|
42
|
+
* perform key-wrapping — they are never surfaced to the caller.
|
|
43
|
+
*/
|
|
44
|
+
export async function encrypt(
|
|
45
|
+
payload: Binary | SymmetricKey,
|
|
46
|
+
key: SymmetricKey,
|
|
47
|
+
iv: Binary,
|
|
48
|
+
algorithm?: AlgorithmUrn
|
|
49
|
+
): Promise<EncryptResult> {
|
|
50
|
+
const crypto = await getVirtruCrypto();
|
|
51
|
+
const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
|
|
52
|
+
|
|
53
|
+
let payloadBytes: Uint8Array;
|
|
54
|
+
if (isSymmetricKey(payload)) {
|
|
55
|
+
// Key-wrapping path: export symmetric key bytes from keystore.
|
|
56
|
+
// Only works if the key was created with extractable:true (e.g. via importSymmetricKey).
|
|
57
|
+
payloadBytes = await crypto.exportKey('raw', unwrapSymmetricKey(payload));
|
|
58
|
+
} else {
|
|
59
|
+
payloadBytes = new Uint8Array(payload.asArrayBuffer());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const vKey = unwrapSymmetricKey(key);
|
|
63
|
+
const ivBytes = new Uint8Array(iv.asArrayBuffer());
|
|
64
|
+
|
|
65
|
+
const cipherBytes = await crypto.encrypt(
|
|
66
|
+
{ name: algName, length: 256, iv: ivBytes } as any,
|
|
67
|
+
vKey,
|
|
68
|
+
payloadBytes
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (algName === 'AES-GCM') {
|
|
72
|
+
return {
|
|
73
|
+
payload: Binary.fromArrayBuffer(cipherBytes.slice(0, -16).buffer as ArrayBuffer),
|
|
74
|
+
authTag: Binary.fromArrayBuffer(cipherBytes.slice(-16).buffer as ArrayBuffer),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { payload: Binary.fromArrayBuffer(cipherBytes.buffer as ArrayBuffer) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decrypt a payload with a symmetric AES key.
|
|
82
|
+
*/
|
|
83
|
+
export async function decrypt(
|
|
84
|
+
payload: Binary,
|
|
85
|
+
key: SymmetricKey,
|
|
86
|
+
iv: Binary,
|
|
87
|
+
algorithm?: AlgorithmUrn,
|
|
88
|
+
authTag?: Binary
|
|
89
|
+
): Promise<DecryptResult> {
|
|
90
|
+
const crypto = await getVirtruCrypto();
|
|
91
|
+
const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
|
|
92
|
+
|
|
93
|
+
let payloadBytes = new Uint8Array(payload.asArrayBuffer());
|
|
94
|
+
const ivBytes = new Uint8Array(iv.asArrayBuffer());
|
|
95
|
+
|
|
96
|
+
if (algName === 'AES-GCM') {
|
|
97
|
+
if (!authTag) {
|
|
98
|
+
throw new Error('Missing authTag for AES-GCM decryption');
|
|
99
|
+
}
|
|
100
|
+
const tagBytes = new Uint8Array(authTag.asArrayBuffer());
|
|
101
|
+
const combined = new Uint8Array(payloadBytes.length + tagBytes.length);
|
|
102
|
+
combined.set(payloadBytes, 0);
|
|
103
|
+
combined.set(tagBytes, payloadBytes.length);
|
|
104
|
+
payloadBytes = combined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const vKey = unwrapSymmetricKey(key);
|
|
108
|
+
const plainBytes = await crypto.decrypt(
|
|
109
|
+
{ name: algName, length: 256, iv: ivBytes } as any,
|
|
110
|
+
vKey,
|
|
111
|
+
payloadBytes
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return { payload: Binary.fromArrayBuffer(plainBytes.buffer as ArrayBuffer) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── HMAC ─────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const HMAC_PARAMS = { name: 'HMAC' as const, hash: 'SHA-256' as const };
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compute HMAC-SHA256.
|
|
123
|
+
* The SymmetricKey must have been created with extractable:true (e.g. via
|
|
124
|
+
* importSymmetricKey) so its bytes can be re-imported as an HMAC key.
|
|
125
|
+
*/
|
|
126
|
+
export async function hmac(data: Uint8Array, key: SymmetricKey): Promise<Uint8Array> {
|
|
127
|
+
const crypto = await getVirtruCrypto();
|
|
128
|
+
const vKey = unwrapSymmetricKey(key);
|
|
129
|
+
// The key was imported with 'sign' usage (see importSymmetricKey).
|
|
130
|
+
// VirtruCrypto sign() checks usage.includes('sign') then calls the WASM
|
|
131
|
+
// hmac() with the slot number — the key type string is irrelevant for HMAC.
|
|
132
|
+
// Do NOT call release() here: the key handle must remain valid for future calls.
|
|
133
|
+
const sig = await crypto.sign(HMAC_PARAMS, vKey, data);
|
|
134
|
+
return new Uint8Array(sig);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Verify HMAC-SHA256 using constant-time comparison.
|
|
139
|
+
*/
|
|
140
|
+
export async function verifyHmac(
|
|
141
|
+
data: Uint8Array,
|
|
142
|
+
signature: Uint8Array,
|
|
143
|
+
key: SymmetricKey
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
const expected = await hmac(data, key);
|
|
146
|
+
if (signature.length !== expected.length) return false;
|
|
147
|
+
let mismatch = 0;
|
|
148
|
+
for (let i = 0; i < signature.length; i++) {
|
|
149
|
+
mismatch |= signature[i] ^ expected[i];
|
|
150
|
+
}
|
|
151
|
+
return mismatch === 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Symmetric Key Import / Split / Merge ─────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Import raw key bytes as an opaque SymmetricKey.
|
|
158
|
+
* The resulting key is extractable so it can be used for HMAC and key-wrapping.
|
|
159
|
+
*/
|
|
160
|
+
export async function importSymmetricKey(keyBytes: Uint8Array): Promise<SymmetricKey> {
|
|
161
|
+
const crypto = await getVirtruCrypto();
|
|
162
|
+
const bitLen = keyBytes.length * 8;
|
|
163
|
+
// Include 'sign'/'verify' so this key can also be used for HMAC-SHA256.
|
|
164
|
+
// VirtruCrypto's sign() checks key.usage.includes('sign'); the underlying
|
|
165
|
+
// WASM hmac() takes the slot number and works with any 256-bit key material.
|
|
166
|
+
const vKey = await crypto.importKey(
|
|
167
|
+
'raw',
|
|
168
|
+
keyBytes,
|
|
169
|
+
{ name: 'AES-GCM' },
|
|
170
|
+
true,
|
|
171
|
+
SYM_KEY_METHODS
|
|
172
|
+
);
|
|
173
|
+
return wrapSymmetricKey(vKey, bitLen);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Split a symmetric key into N shares using XOR secret sharing.
|
|
178
|
+
*
|
|
179
|
+
* For numShares === 1 (single-KAS), the key is returned as-is — no key
|
|
180
|
+
* material is exported and no splitting occurs. This is the common DSP case.
|
|
181
|
+
*
|
|
182
|
+
* For numShares > 1 (multi-KAS), XOR secret sharing requires exporting the
|
|
183
|
+
* raw key bytes. This is not yet implemented in FIPS mode.
|
|
184
|
+
*/
|
|
185
|
+
export async function splitSymmetricKey(key: SymmetricKey, numShares: number): Promise<SymmetricKey[]> {
|
|
186
|
+
if (numShares === 1) {
|
|
187
|
+
return [key];
|
|
188
|
+
}
|
|
189
|
+
throw new ConfigurationError(
|
|
190
|
+
`splitSymmetricKey with ${numShares} shares is not yet supported in FIPS mode. ` +
|
|
191
|
+
'Multi-KAS key splitting requires raw key export which violates FIPS key boundary constraints.'
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Merge symmetric key shares back into the original key using XOR.
|
|
197
|
+
*
|
|
198
|
+
* For a single share (single-KAS), the share is returned as-is — no key
|
|
199
|
+
* material is exported and no XOR occurs. This is the common DSP case.
|
|
200
|
+
*
|
|
201
|
+
* For multiple shares (multi-KAS), XOR merging requires exporting raw key bytes.
|
|
202
|
+
* This is not yet implemented in FIPS mode.
|
|
203
|
+
*/
|
|
204
|
+
export async function mergeSymmetricKeys(shares: SymmetricKey[]): Promise<SymmetricKey> {
|
|
205
|
+
if (shares.length === 0) throw new ConfigurationError('No shares provided to mergeSymmetricKeys');
|
|
206
|
+
if (shares.length === 1) {
|
|
207
|
+
return shares[0];
|
|
208
|
+
}
|
|
209
|
+
throw new ConfigurationError(
|
|
210
|
+
`mergeSymmetricKeys with ${shares.length} shares is not yet supported in FIPS mode. ` +
|
|
211
|
+
'Multi-KAS key merging requires raw key export which violates FIPS key boundary constraints.'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { type CryptoService } from '@opentdf/sdk/singlecontainer';
|
|
8
|
+
|
|
9
|
+
export { initializeFipsCrypto } from './fips-crypto/primitives';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
wrapPublicKey,
|
|
13
|
+
wrapPrivateKey,
|
|
14
|
+
wrapSymmetricKey,
|
|
15
|
+
wrapKeyPair,
|
|
16
|
+
unwrapPublicKey,
|
|
17
|
+
unwrapPrivateKey,
|
|
18
|
+
unwrapSymmetricKey,
|
|
19
|
+
} from './fips-crypto/keys';
|
|
20
|
+
|
|
21
|
+
import { randomBytes, digest } from './fips-crypto/primitives';
|
|
22
|
+
import { generateKey, encrypt, decrypt, hmac, verifyHmac, importSymmetricKey, splitSymmetricKey, mergeSymmetricKeys } from './fips-crypto/symmetric';
|
|
23
|
+
import { generateKeyPair, generateSigningKeyPair, encryptWithPublicKey, decryptWithPrivateKey, sign, verify, generateECKeyPair, deriveKeyFromECDH } from './fips-crypto/asymmetric';
|
|
24
|
+
import { importPublicKey, importPrivateKey, exportPublicKeyPem, exportPrivateKeyPem, exportPublicKeyJwk, parsePublicKeyPem, extractPublicKeyPem, jwkToPublicKeyPem } from './fips-crypto/key-format';
|
|
25
|
+
export { importPrivateKey, exportPrivateKeyPem } from './fips-crypto/key-format';
|
|
26
|
+
|
|
27
|
+
// ─── FipsCryptoService ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const FipsCryptoService: CryptoService = {
|
|
30
|
+
name: 'FipsCryptoService',
|
|
31
|
+
method: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
|
|
32
|
+
decrypt,
|
|
33
|
+
decryptWithPrivateKey,
|
|
34
|
+
encrypt,
|
|
35
|
+
encryptWithPublicKey,
|
|
36
|
+
generateKey,
|
|
37
|
+
generateKeyPair,
|
|
38
|
+
generateSigningKeyPair,
|
|
39
|
+
randomBytes,
|
|
40
|
+
sign,
|
|
41
|
+
verify,
|
|
42
|
+
hmac,
|
|
43
|
+
verifyHmac,
|
|
44
|
+
digest,
|
|
45
|
+
generateECKeyPair,
|
|
46
|
+
deriveKeyFromECDH,
|
|
47
|
+
importPublicKey,
|
|
48
|
+
importPrivateKey,
|
|
49
|
+
exportPublicKeyPem,
|
|
50
|
+
exportPrivateKeyPem,
|
|
51
|
+
exportPublicKeyJwk,
|
|
52
|
+
parsePublicKeyPem,
|
|
53
|
+
extractPublicKeyPem,
|
|
54
|
+
jwkToPublicKeyPem,
|
|
55
|
+
importSymmetricKey,
|
|
56
|
+
splitSymmetricKey,
|
|
57
|
+
mergeSymmetricKeys,
|
|
58
|
+
};
|
package/src/lib/utils.test.ts
CHANGED
|
@@ -15,6 +15,15 @@ vi.mock('@opentdf/sdk', () => ({
|
|
|
15
15
|
PermissionDeniedError: class PermissionDeniedError extends Error {},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
+
vi.mock('@opentdf/sdk/singlecontainer', () => ({
|
|
19
|
+
WebCryptoService: {
|
|
20
|
+
generateSigningKeyPair: vi.fn().mockResolvedValue({
|
|
21
|
+
publicKey: {},
|
|
22
|
+
privateKey: {},
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
18
27
|
vi.mock('@opentdf/sdk/platform', () => ({
|
|
19
28
|
PlatformClient: vi.fn(),
|
|
20
29
|
}));
|
|
@@ -86,6 +95,7 @@ import { validateJWSCertificateChain, getTrustedCertificates } from './utils';
|
|
|
86
95
|
import { DSPClient } from './client';
|
|
87
96
|
import { PlatformClient } from '@opentdf/sdk/platform';
|
|
88
97
|
import { AuthProviders } from '@opentdf/sdk';
|
|
98
|
+
import { WebCryptoService } from '@opentdf/sdk/singlecontainer';
|
|
89
99
|
|
|
90
100
|
describe('validateJWSCertificateChain', () => {
|
|
91
101
|
beforeEach(() => {
|
|
@@ -354,15 +364,8 @@ describe('getTrustedCertificates', () => {
|
|
|
354
364
|
await getTrustedCertificates(mockOptions);
|
|
355
365
|
|
|
356
366
|
expect(AuthProviders.refreshAuthProvider).toHaveBeenCalled();
|
|
367
|
+
expect(WebCryptoService.generateSigningKeyPair).toHaveBeenCalled();
|
|
357
368
|
expect(mockAuthProvider.updateClientPublicKey).toHaveBeenCalled();
|
|
358
|
-
expect(globalThis.crypto.subtle.generateKey).toHaveBeenCalledWith(
|
|
359
|
-
{
|
|
360
|
-
name: 'ECDSA',
|
|
361
|
-
namedCurve: 'P-256',
|
|
362
|
-
},
|
|
363
|
-
true,
|
|
364
|
-
['sign', 'verify']
|
|
365
|
-
);
|
|
366
369
|
});
|
|
367
370
|
|
|
368
371
|
it('creates both PlatformClient and DSPClient with correct config', async () => {
|
package/src/lib/utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
PermissionDeniedError,
|
|
12
12
|
type Source,
|
|
13
13
|
} from '@opentdf/sdk';
|
|
14
|
+
import { type CryptoService, WebCryptoService } from '@opentdf/sdk/singlecontainer';
|
|
14
15
|
import { PlatformClient } from '@opentdf/sdk/platform';
|
|
15
16
|
import { ActiveStateEnum } from '@opentdf/sdk/platform/common/common_pb.js';
|
|
16
17
|
import type { Certificate } from '../gen/virtru/policy/objects_pb';
|
|
@@ -50,7 +51,7 @@ export type GetUserEntitlementsResponse = {
|
|
|
50
51
|
* @returns An `Interceptor` function that modifies requests to include authentication headers.
|
|
51
52
|
*/
|
|
52
53
|
|
|
53
|
-
export function createAuthInterceptor(authProvider: AuthProvider): Interceptor {
|
|
54
|
+
export function createAuthInterceptor(authProvider: AuthProvider, _cryptoService?: CryptoService): Interceptor {
|
|
54
55
|
return (next) => async (req) => {
|
|
55
56
|
const url = new URL(req.url);
|
|
56
57
|
const pathOnly = url.pathname;
|
|
@@ -79,6 +80,7 @@ export async function createAuthProvider(options: {
|
|
|
79
80
|
userInfoEndpoint: string;
|
|
80
81
|
};
|
|
81
82
|
refreshToken: string;
|
|
83
|
+
cryptoService?: CryptoService;
|
|
82
84
|
}): Promise<AuthProviders.OIDCRefreshTokenProvider> {
|
|
83
85
|
const { oidc, refreshToken } = options;
|
|
84
86
|
|
|
@@ -102,6 +104,7 @@ export async function createOpenTDFClient(options: {
|
|
|
102
104
|
platformEndpoint: string;
|
|
103
105
|
refreshToken: string;
|
|
104
106
|
obligations: string[];
|
|
107
|
+
cryptoService?: CryptoService;
|
|
105
108
|
}) {
|
|
106
109
|
const { platformEndpoint, obligations, ...authProviderOptions } = options;
|
|
107
110
|
return new OpenTDF({
|
|
@@ -117,6 +120,7 @@ export async function createOpenTDFClient(options: {
|
|
|
117
120
|
noVerify: true,
|
|
118
121
|
},
|
|
119
122
|
disableDPoP: true,
|
|
123
|
+
cryptoService: options.cryptoService,
|
|
120
124
|
});
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -131,6 +135,7 @@ export const getObligations = async (
|
|
|
131
135
|
platformEndpoint: string;
|
|
132
136
|
refreshToken: string;
|
|
133
137
|
obligations: string[];
|
|
138
|
+
cryptoService?: CryptoService;
|
|
134
139
|
}
|
|
135
140
|
): Promise<string[]> => {
|
|
136
141
|
const client = await createOpenTDFClient(options);
|
|
@@ -159,10 +164,12 @@ export async function getUserEntitlements(options: {
|
|
|
159
164
|
};
|
|
160
165
|
platformEndpoint: string;
|
|
161
166
|
refreshToken: string;
|
|
167
|
+
cryptoService?: CryptoService;
|
|
162
168
|
}) {
|
|
163
169
|
const authProvider = await createAuthProvider({
|
|
164
170
|
oidc: options.oidc,
|
|
165
171
|
refreshToken: options.refreshToken,
|
|
172
|
+
cryptoService: options.cryptoService,
|
|
166
173
|
});
|
|
167
174
|
const accessToken = await authProvider.oidcAuth.get();
|
|
168
175
|
const userEntitlementsResponse = await fetch(
|
|
@@ -271,23 +278,16 @@ export async function getTrustedCertificates(options: {
|
|
|
271
278
|
};
|
|
272
279
|
platformEndpoint: string;
|
|
273
280
|
refreshToken: string;
|
|
281
|
+
cryptoService?: CryptoService;
|
|
274
282
|
}): Promise<Certificate[]> {
|
|
275
|
-
|
|
276
|
-
return crypto.subtle.generateKey(
|
|
277
|
-
{
|
|
278
|
-
name: 'ECDSA',
|
|
279
|
-
namedCurve: 'P-256',
|
|
280
|
-
},
|
|
281
|
-
true,
|
|
282
|
-
['sign', 'verify']
|
|
283
|
-
);
|
|
284
|
-
}
|
|
283
|
+
const cs = options.cryptoService ?? WebCryptoService;
|
|
285
284
|
|
|
286
285
|
const authProvider = await createAuthProvider({
|
|
287
286
|
oidc: options.oidc,
|
|
288
287
|
refreshToken: options.refreshToken,
|
|
288
|
+
cryptoService: options.cryptoService,
|
|
289
289
|
});
|
|
290
|
-
await authProvider.updateClientPublicKey(await generateSigningKeyPair());
|
|
290
|
+
await authProvider.updateClientPublicKey(await cs.generateSigningKeyPair());
|
|
291
291
|
|
|
292
292
|
// Create both clients
|
|
293
293
|
const platform = new PlatformClient({
|
|
@@ -423,7 +423,8 @@ function pemToArrayBuffer(pem: string): ArrayBuffer {
|
|
|
423
423
|
*/
|
|
424
424
|
export async function validateJWSCertificateChain(
|
|
425
425
|
x5c: string[],
|
|
426
|
-
trustedRootCertificates: Certificate[]
|
|
426
|
+
trustedRootCertificates: Certificate[],
|
|
427
|
+
_cryptoService?: CryptoService
|
|
427
428
|
): Promise<JWSValidationResult> {
|
|
428
429
|
const warnings: string[] = [];
|
|
429
430
|
|
|
@@ -21,10 +21,18 @@
|
|
|
21
21
|
"tests/dsp.test.ts",
|
|
22
22
|
"72K1himJItp7yd9dkZNa7RwUmQo=_KBnhlqHDJygGymeSAMhfjnUj064="
|
|
23
23
|
],
|
|
24
|
+
[
|
|
25
|
+
"tests/fips-crypto.unit.test.ts",
|
|
26
|
+
"eStngPGfsHlKIOfJ2k1WkUpqFX0=_KBnhlqHDJygGymeSAMhfjnUj064="
|
|
27
|
+
],
|
|
24
28
|
[
|
|
25
29
|
"tests/setup-msw.ts",
|
|
26
30
|
"Sz1wiT3itCcl8Bpq5hnfs4hPjuo=_KBnhlqHDJygGymeSAMhfjnUj064="
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"tests/setup-unit.ts",
|
|
34
|
+
"Hb3n5KLyWlQyrMYmNx9grgPHBCU=_KBnhlqHDJygGymeSAMhfjnUj064="
|
|
27
35
|
]
|
|
28
36
|
],
|
|
29
|
-
"filesHash": "
|
|
37
|
+
"filesHash": "flOfsEjcUy5GStpiBEHpfg"
|
|
30
38
|
}
|