@opentdf/sdk 0.12.0 → 0.13.0-beta.119

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 (43) hide show
  1. package/dist/cjs/src/auth/dpop.js +4 -4
  2. package/dist/cjs/src/version.js +1 -1
  3. package/dist/cjs/tdf3/src/crypto/core/ec.js +88 -0
  4. package/dist/cjs/tdf3/src/crypto/core/key-format.js +359 -0
  5. package/dist/cjs/tdf3/src/crypto/core/keys.js +85 -0
  6. package/dist/cjs/tdf3/src/crypto/core/rsa.js +120 -0
  7. package/dist/cjs/tdf3/src/crypto/core/signing.js +178 -0
  8. package/dist/cjs/tdf3/src/crypto/core/symmetric.js +205 -0
  9. package/dist/cjs/tdf3/src/crypto/index.js +69 -1051
  10. package/dist/types/src/version.d.ts +1 -1
  11. package/dist/types/tdf3/src/crypto/core/ec.d.ts +11 -0
  12. package/dist/types/tdf3/src/crypto/core/ec.d.ts.map +1 -0
  13. package/dist/types/tdf3/src/crypto/core/key-format.d.ts +41 -0
  14. package/dist/types/tdf3/src/crypto/core/key-format.d.ts.map +1 -0
  15. package/dist/types/tdf3/src/crypto/core/keys.d.ts +27 -0
  16. package/dist/types/tdf3/src/crypto/core/keys.d.ts.map +1 -0
  17. package/dist/types/tdf3/src/crypto/core/rsa.d.ts +35 -0
  18. package/dist/types/tdf3/src/crypto/core/rsa.d.ts.map +1 -0
  19. package/dist/types/tdf3/src/crypto/core/signing.d.ts +10 -0
  20. package/dist/types/tdf3/src/crypto/core/signing.d.ts.map +1 -0
  21. package/dist/types/tdf3/src/crypto/core/symmetric.d.ts +68 -0
  22. package/dist/types/tdf3/src/crypto/core/symmetric.d.ts.map +1 -0
  23. package/dist/types/tdf3/src/crypto/index.d.ts +11 -164
  24. package/dist/types/tdf3/src/crypto/index.d.ts.map +1 -1
  25. package/dist/web/src/auth/dpop.js +4 -4
  26. package/dist/web/src/version.js +1 -1
  27. package/dist/web/tdf3/src/crypto/core/ec.js +84 -0
  28. package/dist/web/tdf3/src/crypto/core/key-format.js +348 -0
  29. package/dist/web/tdf3/src/crypto/core/keys.js +78 -0
  30. package/dist/web/tdf3/src/crypto/core/rsa.js +112 -0
  31. package/dist/web/tdf3/src/crypto/core/signing.js +174 -0
  32. package/dist/web/tdf3/src/crypto/core/symmetric.js +192 -0
  33. package/dist/web/tdf3/src/crypto/index.js +13 -994
  34. package/package.json +1 -1
  35. package/src/auth/dpop.ts +3 -3
  36. package/src/version.ts +1 -1
  37. package/tdf3/src/crypto/core/ec.ts +118 -0
  38. package/tdf3/src/crypto/core/key-format.ts +420 -0
  39. package/tdf3/src/crypto/core/keys.ts +86 -0
  40. package/tdf3/src/crypto/core/rsa.ts +144 -0
  41. package/tdf3/src/crypto/core/signing.ts +214 -0
  42. package/tdf3/src/crypto/core/symmetric.ts +265 -0
  43. package/tdf3/src/crypto/index.ts +71 -1239
@@ -0,0 +1,144 @@
1
+ import { Binary } from '../../binary.js';
2
+ import {
3
+ type KeyAlgorithm,
4
+ type KeyPair,
5
+ MIN_ASYMMETRIC_KEY_SIZE_BITS,
6
+ type PrivateKey,
7
+ type PublicKey,
8
+ type SymmetricKey,
9
+ } from '../declarations.js';
10
+ import { ConfigurationError } from '../../../../src/errors.js';
11
+ import { unwrapKey, unwrapSymmetricKey, wrapPrivateKey, wrapPublicKey } from './keys.js';
12
+
13
+ const ENC_DEC_METHODS: KeyUsage[] = ['encrypt', 'decrypt'];
14
+ const SIGN_VERIFY_METHODS: KeyUsage[] = ['sign', 'verify'];
15
+
16
+ /**
17
+ * Get a DOMString representing the algorithm to use for an
18
+ * asymmetric key generation.
19
+ */
20
+ export function rsaOaepSha1(
21
+ modulusLength: number = MIN_ASYMMETRIC_KEY_SIZE_BITS
22
+ ): RsaHashedKeyGenParams {
23
+ if (!modulusLength || modulusLength < MIN_ASYMMETRIC_KEY_SIZE_BITS) {
24
+ throw new ConfigurationError('Invalid key size requested');
25
+ }
26
+ return {
27
+ name: 'RSA-OAEP',
28
+ hash: {
29
+ name: 'SHA-1',
30
+ },
31
+ modulusLength,
32
+ // 24 bit representation of 65537
33
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
34
+ };
35
+ }
36
+
37
+ export function rsaPkcs1Sha256(
38
+ modulusLength: number = MIN_ASYMMETRIC_KEY_SIZE_BITS
39
+ ): RsaHashedKeyGenParams {
40
+ if (!modulusLength || modulusLength < MIN_ASYMMETRIC_KEY_SIZE_BITS) {
41
+ throw new ConfigurationError('Invalid key size requested');
42
+ }
43
+ return {
44
+ name: 'RSASSA-PKCS1-v1_5',
45
+ hash: {
46
+ name: 'SHA-256',
47
+ },
48
+ modulusLength,
49
+ // 24 bit representation of 65537
50
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Generate an RSA key pair
56
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey}
57
+ * @param size in bits
58
+ */
59
+ export async function generateKeyPair(size?: number): Promise<KeyPair> {
60
+ const keySize = size || MIN_ASYMMETRIC_KEY_SIZE_BITS;
61
+ const algoDomString = rsaOaepSha1(keySize);
62
+ const keyPair = await crypto.subtle.generateKey(algoDomString, true, ENC_DEC_METHODS);
63
+
64
+ // Map to supported algorithm sizes
65
+ let algorithm: KeyAlgorithm;
66
+ if (keySize === 2048) {
67
+ algorithm = 'rsa:2048';
68
+ } else if (keySize === 4096) {
69
+ algorithm = 'rsa:4096';
70
+ } else {
71
+ throw new ConfigurationError(
72
+ `Unsupported RSA key size: ${keySize}. Only 2048 and 4096 are supported.`
73
+ );
74
+ }
75
+
76
+ return {
77
+ publicKey: wrapPublicKey(keyPair.publicKey, algorithm),
78
+ privateKey: wrapPrivateKey(keyPair.privateKey, algorithm),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Generate an RSA key pair suitable for signatures
84
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey}
85
+ */
86
+ export async function generateSigningKeyPair(): Promise<KeyPair> {
87
+ const rsaParams = rsaPkcs1Sha256(2048);
88
+ const keyPair = await crypto.subtle.generateKey(rsaParams, true, SIGN_VERIFY_METHODS);
89
+
90
+ const algorithm: KeyAlgorithm = 'rsa:2048';
91
+ return {
92
+ publicKey: wrapPublicKey(keyPair.publicKey, algorithm),
93
+ privateKey: wrapPrivateKey(keyPair.privateKey, algorithm),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Encrypt using a public key (RSA-OAEP).
99
+ * Accepts Binary or SymmetricKey for key wrapping.
100
+ * @param payload Payload to encrypt (Binary) or symmetric key to wrap (SymmetricKey)
101
+ * @param publicKey Opaque public key
102
+ * @return Encrypted payload
103
+ */
104
+ export async function encryptWithPublicKey(
105
+ payload: Binary | SymmetricKey,
106
+ publicKey: PublicKey
107
+ ): Promise<Binary> {
108
+ let payloadBuffer: BufferSource;
109
+
110
+ // Handle SymmetricKey unwrapping
111
+ if ('_brand' in payload && payload._brand === 'SymmetricKey') {
112
+ // Pass Uint8Array directly — Web Crypto respects byteOffset/byteLength on typed array views.
113
+ payloadBuffer = unwrapSymmetricKey(payload);
114
+ } else {
115
+ // Binary payload
116
+ payloadBuffer = (payload as Binary).asArrayBuffer();
117
+ }
118
+
119
+ const cryptoKey = unwrapKey(publicKey);
120
+ const result = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, cryptoKey, payloadBuffer);
121
+ return Binary.fromArrayBuffer(result);
122
+ }
123
+
124
+ /**
125
+ * Decrypt a public-key encrypted payload with a private key
126
+ * @param encryptedPayload Payload to decrypt
127
+ * @param privateKey Opaque private key
128
+ * @return Decrypted payload
129
+ */
130
+ export async function decryptWithPrivateKey(
131
+ encryptedPayload: Binary,
132
+ privateKey: PrivateKey
133
+ ): Promise<Binary> {
134
+ console.assert(typeof encryptedPayload === 'object', 'encryptedPayload must be object');
135
+
136
+ const cryptoKey = unwrapKey(privateKey);
137
+ const payload = await crypto.subtle.decrypt(
138
+ { name: 'RSA-OAEP' },
139
+ cryptoKey,
140
+ encryptedPayload.asArrayBuffer()
141
+ );
142
+ const bufferView = new Uint8Array(payload);
143
+ return Binary.fromArrayBuffer(bufferView.buffer);
144
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ type AsymmetricSigningAlgorithm,
3
+ type PrivateKey,
4
+ type PublicKey,
5
+ } from '../declarations.js';
6
+ import { ConfigurationError } from '../../../../src/errors.js';
7
+ import { unwrapKey } from './keys.js';
8
+
9
+ /**
10
+ * Get the Web Crypto algorithm parameters for a signing algorithm.
11
+ */
12
+ function getSigningAlgorithmParams(algorithm: AsymmetricSigningAlgorithm): {
13
+ importParams: RsaHashedImportParams | EcKeyImportParams;
14
+ signParams: AlgorithmIdentifier | EcdsaParams;
15
+ } {
16
+ switch (algorithm) {
17
+ case 'RS256':
18
+ return {
19
+ importParams: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
20
+ signParams: 'RSASSA-PKCS1-v1_5',
21
+ };
22
+ case 'ES256':
23
+ return {
24
+ importParams: { name: 'ECDSA', namedCurve: 'P-256' },
25
+ signParams: { name: 'ECDSA', hash: 'SHA-256' } as EcdsaParams,
26
+ };
27
+ case 'ES384':
28
+ return {
29
+ importParams: { name: 'ECDSA', namedCurve: 'P-384' },
30
+ signParams: { name: 'ECDSA', hash: 'SHA-384' } as EcdsaParams,
31
+ };
32
+ case 'ES512':
33
+ return {
34
+ importParams: { name: 'ECDSA', namedCurve: 'P-521' },
35
+ signParams: { name: 'ECDSA', hash: 'SHA-512' } as EcdsaParams,
36
+ };
37
+ default:
38
+ throw new ConfigurationError(`Unsupported signing algorithm: ${algorithm}`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Convert IEEE P1363 signature format (used by WebCrypto ECDSA) to DER format (used by JWT).
44
+ * RS256 signatures don't need conversion.
45
+ */
46
+ function ieeeP1363ToDer(signature: Uint8Array, algorithm: AsymmetricSigningAlgorithm): Uint8Array {
47
+ if (algorithm === 'RS256') {
48
+ return signature;
49
+ }
50
+
51
+ // IEEE P1363: r || s where each is padded to key size
52
+ const halfLen = signature.length / 2;
53
+ const r = signature.slice(0, halfLen);
54
+ const s = signature.slice(halfLen);
55
+
56
+ // Remove leading zeros but keep one if the high bit is set
57
+ const trimLeadingZeros = (arr: Uint8Array): Uint8Array => {
58
+ let i = 0;
59
+ while (i < arr.length - 1 && arr[i] === 0) i++;
60
+ return arr.slice(i);
61
+ };
62
+
63
+ let rTrimmed = trimLeadingZeros(r);
64
+ let sTrimmed = trimLeadingZeros(s);
65
+
66
+ // Add leading zero if high bit is set (to keep positive in DER)
67
+ if (rTrimmed[0] & 0x80) {
68
+ const padded = new Uint8Array(rTrimmed.length + 1);
69
+ padded.set(rTrimmed, 1);
70
+ rTrimmed = padded;
71
+ }
72
+ if (sTrimmed[0] & 0x80) {
73
+ const padded = new Uint8Array(sTrimmed.length + 1);
74
+ padded.set(sTrimmed, 1);
75
+ sTrimmed = padded;
76
+ }
77
+
78
+ // DER SEQUENCE: 0x30 [length] [r INTEGER] [s INTEGER]
79
+ // INTEGER: 0x02 [length] [value]
80
+ const rDer = new Uint8Array([0x02, rTrimmed.length, ...rTrimmed]);
81
+ const sDer = new Uint8Array([0x02, sTrimmed.length, ...sTrimmed]);
82
+
83
+ const seqLen = rDer.length + sDer.length;
84
+ // DER length: short-form for < 128, long-form (0x81 nn) for 128-255.
85
+ // ECDSA sequences never exceed 255 bytes for any supported curve.
86
+ const lenBytes = seqLen < 128 ? new Uint8Array([seqLen]) : new Uint8Array([0x81, seqLen]);
87
+ const result = new Uint8Array(1 + lenBytes.length + seqLen);
88
+ result[0] = 0x30;
89
+ result.set(lenBytes, 1);
90
+ result.set(rDer, 1 + lenBytes.length);
91
+ result.set(sDer, 1 + lenBytes.length + rDer.length);
92
+
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * Convert DER signature format (used by JWT) to IEEE P1363 format (used by WebCrypto ECDSA).
98
+ * RS256 signatures don't need conversion.
99
+ */
100
+ function derToIeeeP1363(signature: Uint8Array, algorithm: AsymmetricSigningAlgorithm): Uint8Array {
101
+ if (algorithm === 'RS256') {
102
+ return signature;
103
+ }
104
+
105
+ // Determine the expected component length based on algorithm
106
+ let componentLen: number;
107
+ switch (algorithm) {
108
+ case 'ES256':
109
+ componentLen = 32;
110
+ break;
111
+ case 'ES384':
112
+ componentLen = 48;
113
+ break;
114
+ case 'ES512':
115
+ componentLen = 66;
116
+ break;
117
+ default:
118
+ throw new ConfigurationError(`Unsupported algorithm for DER conversion: ${algorithm}`);
119
+ }
120
+
121
+ if (signature[0] !== 0x30) {
122
+ throw new ConfigurationError('Invalid DER signature: expected SEQUENCE');
123
+ }
124
+
125
+ // Skip SEQUENCE tag, then parse DER length (short- or long-form).
126
+ let offset = 1;
127
+ if (signature[offset] & 0x80) {
128
+ // Long-form: low 7 bits = number of subsequent length bytes.
129
+ const lenBytesCount = signature[offset] & 0x7f;
130
+ if (lenBytesCount === 0 || lenBytesCount > 4) {
131
+ throw new ConfigurationError('Invalid DER signature: invalid long-form length');
132
+ }
133
+ offset += 1 + lenBytesCount;
134
+ if (offset > signature.length) {
135
+ throw new ConfigurationError('Invalid DER signature: length bytes exceed signature length');
136
+ }
137
+ } else {
138
+ // Short-form: single length byte.
139
+ offset += 1;
140
+ }
141
+
142
+ // Parse r INTEGER
143
+ if (signature[offset] !== 0x02) {
144
+ throw new ConfigurationError('Invalid DER signature: expected INTEGER for r');
145
+ }
146
+ const rLen = signature[offset + 1];
147
+ offset += 2;
148
+ let r = signature.slice(offset, offset + rLen);
149
+ offset += rLen;
150
+
151
+ // Parse s INTEGER
152
+ if (signature[offset] !== 0x02) {
153
+ throw new ConfigurationError('Invalid DER signature: expected INTEGER for s');
154
+ }
155
+ const sLen = signature[offset + 1];
156
+ offset += 2;
157
+ let s = signature.slice(offset, offset + sLen);
158
+
159
+ // Remove leading zero padding if present
160
+ if (r[0] === 0 && r.length > componentLen) {
161
+ r = r.slice(1);
162
+ }
163
+ if (s[0] === 0 && s.length > componentLen) {
164
+ s = s.slice(1);
165
+ }
166
+
167
+ // Pad to component length
168
+ const result = new Uint8Array(componentLen * 2);
169
+ result.set(r, componentLen - r.length);
170
+ result.set(s, componentLen * 2 - s.length);
171
+
172
+ return result;
173
+ }
174
+
175
+ /**
176
+ * Sign data with an asymmetric private key.
177
+ */
178
+ export async function sign(
179
+ data: Uint8Array,
180
+ privateKey: PrivateKey,
181
+ algorithm: AsymmetricSigningAlgorithm
182
+ ): Promise<Uint8Array> {
183
+ const { signParams } = getSigningAlgorithmParams(algorithm);
184
+
185
+ // Unwrap the internal CryptoKey
186
+ const key = unwrapKey(privateKey);
187
+
188
+ // Sign the data
189
+ const signature = await crypto.subtle.sign(signParams, key, data);
190
+
191
+ // Convert from IEEE P1363 to DER for EC algorithms
192
+ return ieeeP1363ToDer(new Uint8Array(signature), algorithm);
193
+ }
194
+
195
+ /**
196
+ * Verify signature with an asymmetric public key.
197
+ */
198
+ export async function verify(
199
+ data: Uint8Array,
200
+ signature: Uint8Array,
201
+ publicKey: PublicKey,
202
+ algorithm: AsymmetricSigningAlgorithm
203
+ ): Promise<boolean> {
204
+ const { signParams } = getSigningAlgorithmParams(algorithm);
205
+
206
+ // Unwrap the internal CryptoKey
207
+ const key = unwrapKey(publicKey);
208
+
209
+ // Convert from DER to IEEE P1363 for EC algorithms
210
+ const ieeeSignature = derToIeeeP1363(signature, algorithm);
211
+
212
+ // Verify the signature
213
+ return crypto.subtle.verify(signParams, key, ieeeSignature, data);
214
+ }
@@ -0,0 +1,265 @@
1
+ import { Algorithms, type AlgorithmUrn } from '../../ciphers/algorithms.js';
2
+ import { Binary } from '../../binary.js';
3
+ import {
4
+ type DecryptResult,
5
+ type EncryptResult,
6
+ type HashAlgorithm,
7
+ type SymmetricKey,
8
+ } from '../declarations.js';
9
+ import { ConfigurationError, DecryptError } from '../../../../src/errors.js';
10
+ import { encodeArrayBuffer as hexEncode } from '../../../../src/encodings/hex.js';
11
+ import { keyMerge } from '../../utils/keysplit.js';
12
+ import { unwrapSymmetricKey, wrapSymmetricKey } from './keys.js';
13
+
14
+ const ENC_DEC_METHODS: KeyUsage[] = ['encrypt', 'decrypt'];
15
+
16
+ /**
17
+ * Generate a random symmetric key (opaque).
18
+ * @param length - Key length in bytes (default 32 for AES-256)
19
+ * @return Opaque symmetric key
20
+ */
21
+ export async function generateKey(length?: number): Promise<SymmetricKey> {
22
+ const keyBytes = await randomBytes(length || 32);
23
+ return wrapSymmetricKey(keyBytes);
24
+ }
25
+
26
+ export async function randomBytes(byteLength: number): Promise<Uint8Array> {
27
+ const r = new Uint8Array(byteLength);
28
+ crypto.getRandomValues(r);
29
+ return r;
30
+ }
31
+
32
+ /**
33
+ * Returns a promise to the encryption key as a binary string.
34
+ *
35
+ * Note: This function should almost never fail as it includes a fallback
36
+ * if for some reason the native generate key fails.
37
+ *
38
+ * @param length The key length, defaults to 256
39
+ *
40
+ * @returns The hex string.
41
+ */
42
+ export async function randomBytesAsHex(length: number): Promise<string> {
43
+ // Create a typed array of the correct length to fill
44
+ const r = new Uint8Array(length);
45
+ crypto.getRandomValues(r);
46
+ return hexEncode(r.buffer);
47
+ }
48
+
49
+ /**
50
+ * Decrypt content synchronously
51
+ * @param payload The payload to decrypt
52
+ * @param key The symmetric encryption key (opaque)
53
+ * @param iv The initialization vector
54
+ * @param algorithm The algorithm to use for encryption
55
+ * @param authTag The authentication tag for authenticated crypto.
56
+ */
57
+ export function decrypt(
58
+ payload: Binary,
59
+ key: SymmetricKey,
60
+ iv: Binary,
61
+ algorithm?: AlgorithmUrn,
62
+ authTag?: Binary
63
+ ): Promise<DecryptResult> {
64
+ return _doDecrypt(payload, key, iv, algorithm, authTag);
65
+ }
66
+
67
+ /**
68
+ * Encrypt content synchronously
69
+ * @param payload The payload to encrypt
70
+ * @param key The encryption key
71
+ * @param iv The initialization vector
72
+ * @param algorithm The algorithm to use for encryption
73
+ */
74
+ export function encrypt(
75
+ payload: Binary | SymmetricKey,
76
+ key: SymmetricKey,
77
+ iv: Binary,
78
+ algorithm?: AlgorithmUrn
79
+ ): Promise<EncryptResult> {
80
+ return _doEncrypt(payload, key, iv, algorithm);
81
+ }
82
+
83
+ async function _doEncrypt(
84
+ payload: Binary | SymmetricKey,
85
+ key: SymmetricKey,
86
+ iv: Binary,
87
+ algorithm?: AlgorithmUrn
88
+ ): Promise<EncryptResult> {
89
+ console.assert(payload != null);
90
+ console.assert(key != null);
91
+ console.assert(iv != null);
92
+
93
+ // Handle both Binary and SymmetricKey payloads
94
+ let payloadBuffer: BufferSource;
95
+ if ('_brand' in payload && payload._brand === 'SymmetricKey') {
96
+ // Pass Uint8Array directly — Web Crypto respects byteOffset/byteLength on typed array views.
97
+ payloadBuffer = unwrapSymmetricKey(payload);
98
+ } else {
99
+ // Binary payload
100
+ payloadBuffer = (payload as Binary).asArrayBuffer();
101
+ }
102
+
103
+ const algoDomString = getSymmetricAlgoDomString(iv, algorithm);
104
+ const keyBytes = unwrapSymmetricKey(key);
105
+ const importedKey = await _importKey(keyBytes, algoDomString);
106
+ const encrypted = await crypto.subtle.encrypt(algoDomString, importedKey, payloadBuffer);
107
+ if (algoDomString.name === 'AES-GCM') {
108
+ return {
109
+ payload: Binary.fromArrayBuffer(encrypted.slice(0, -16)),
110
+ authTag: Binary.fromArrayBuffer(encrypted.slice(-16)),
111
+ };
112
+ }
113
+ return {
114
+ payload: Binary.fromArrayBuffer(encrypted),
115
+ };
116
+ }
117
+
118
+ async function _doDecrypt(
119
+ payload: Binary,
120
+ key: SymmetricKey,
121
+ iv: Binary,
122
+ algorithm?: AlgorithmUrn,
123
+ authTag?: Binary
124
+ ): Promise<DecryptResult> {
125
+ console.assert(payload != null);
126
+ console.assert(key != null);
127
+ console.assert(iv != null);
128
+
129
+ let payloadBuffer = payload.asArrayBuffer();
130
+
131
+ // Concat the the auth tag to the payload for decryption
132
+ if (authTag) {
133
+ const authTagBuffer = authTag.asArrayBuffer();
134
+ const gcmPayload = new Uint8Array(payloadBuffer.byteLength + authTagBuffer.byteLength);
135
+ gcmPayload.set(new Uint8Array(payloadBuffer), 0);
136
+ gcmPayload.set(new Uint8Array(authTagBuffer), payloadBuffer.byteLength);
137
+ payloadBuffer = gcmPayload.buffer;
138
+ }
139
+
140
+ const algoDomString = getSymmetricAlgoDomString(iv, algorithm);
141
+ const keyBytes = unwrapSymmetricKey(key);
142
+ const importedKey = await _importKey(keyBytes, algoDomString);
143
+ algoDomString.iv = iv.asArrayBuffer();
144
+
145
+ const decrypted = await crypto.subtle
146
+ .decrypt(algoDomString, importedKey, payloadBuffer)
147
+ // Catching this error so we can specifically check for OperationError
148
+ .catch((err) => {
149
+ if (err.name === 'OperationError') {
150
+ throw new DecryptError(err);
151
+ }
152
+
153
+ throw err;
154
+ });
155
+ return { payload: Binary.fromArrayBuffer(decrypted) };
156
+ }
157
+
158
+ function _importKey(keyBytes: Uint8Array, algorithm: AesCbcParams | AesGcmParams) {
159
+ return crypto.subtle.importKey('raw', keyBytes, algorithm, true, ENC_DEC_METHODS);
160
+ }
161
+
162
+ /**
163
+ * Get a DOMString representing the algorithm to use for a crypto
164
+ * operation. Defaults to AES-CBC.
165
+ * @param {String|undefined} algorithm
166
+ * @return {DOMString} Algorithm to use
167
+ */
168
+ function getSymmetricAlgoDomString(
169
+ iv: Binary,
170
+ algorithm?: AlgorithmUrn
171
+ ): AesCbcParams | AesGcmParams {
172
+ let nativeAlgorithm = 'AES-CBC';
173
+ if (algorithm === Algorithms.AES_256_GCM) {
174
+ nativeAlgorithm = 'AES-GCM';
175
+ }
176
+
177
+ return {
178
+ name: nativeAlgorithm,
179
+ iv: iv.asArrayBuffer(),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Create an ArrayBuffer from a hex string.
185
+ * https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String?hl=en
186
+ * @param hex - Hex string
187
+ */
188
+ export function hex2Ab(hex: string): ArrayBuffer {
189
+ const buffer = new ArrayBuffer(hex.length / 2);
190
+ const bufferView = new Uint8Array(buffer);
191
+
192
+ for (let i = 0; i < hex.length; i += 2) {
193
+ bufferView[i / 2] = parseInt(hex.substr(i, 2), 16);
194
+ }
195
+
196
+ return buffer;
197
+ }
198
+
199
+ /**
200
+ * Compute hash digest.
201
+ */
202
+ export async function digest(algorithm: HashAlgorithm, data: Uint8Array): Promise<Uint8Array> {
203
+ const validAlgorithms: HashAlgorithm[] = ['SHA-256', 'SHA-384', 'SHA-512'];
204
+ if (!validAlgorithms.includes(algorithm)) {
205
+ throw new ConfigurationError(`Unsupported hash algorithm: ${algorithm}`);
206
+ }
207
+
208
+ const hashBuffer = await crypto.subtle.digest(algorithm, data);
209
+ return new Uint8Array(hashBuffer);
210
+ }
211
+
212
+ /**
213
+ * Compute HMAC-SHA256 of data with a symmetric key.
214
+ */
215
+ export async function hmac(data: Uint8Array, key: SymmetricKey): Promise<Uint8Array> {
216
+ const keyBytes = unwrapSymmetricKey(key);
217
+ const cryptoKey = await crypto.subtle.importKey(
218
+ 'raw',
219
+ keyBytes,
220
+ { name: 'HMAC', hash: 'SHA-256' },
221
+ false,
222
+ ['sign']
223
+ );
224
+
225
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
226
+ return new Uint8Array(signature);
227
+ }
228
+
229
+ /**
230
+ * Verify HMAC-SHA256.
231
+ * Standalone utility — not part of CryptoService interface.
232
+ */
233
+ export async function verifyHmac(
234
+ data: Uint8Array,
235
+ signature: Uint8Array,
236
+ key: SymmetricKey
237
+ ): Promise<boolean> {
238
+ const keyBytes = unwrapSymmetricKey(key);
239
+ const cryptoKey = await crypto.subtle.importKey(
240
+ 'raw',
241
+ keyBytes,
242
+ { name: 'HMAC', hash: 'SHA-256' },
243
+ false,
244
+ ['verify']
245
+ );
246
+ return crypto.subtle.verify('HMAC', cryptoKey, signature, data);
247
+ }
248
+
249
+ /**
250
+ * Import raw key bytes as an opaque symmetric key.
251
+ * Used for external keys (e.g., unwrapped from KAS).
252
+ */
253
+ export async function importSymmetricKey(keyBytes: Uint8Array): Promise<SymmetricKey> {
254
+ return wrapSymmetricKey(keyBytes);
255
+ }
256
+
257
+ /**
258
+ * Merge symmetric key shares back into the original key using XOR.
259
+ * Key bytes are extracted internally for merging.
260
+ */
261
+ export async function mergeSymmetricKeys(shares: SymmetricKey[]): Promise<SymmetricKey> {
262
+ const splitBytes = shares.map(unwrapSymmetricKey);
263
+ const merged = keyMerge(splitBytes);
264
+ return wrapSymmetricKey(merged);
265
+ }