@oari/jose 0.0.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/LICENSE.md +21 -0
- package/README.md +150 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/jwe/compact/decrypt.d.ts +43 -0
- package/dist/types/jwe/compact/encrypt.d.ts +76 -0
- package/dist/types/jwe/flattened/decrypt.d.ts +53 -0
- package/dist/types/jwe/flattened/encrypt.d.ts +95 -0
- package/dist/types/jwe/general/decrypt.d.ts +64 -0
- package/dist/types/jwe/general/encrypt.d.ts +89 -0
- package/dist/types/jwk/embedded.d.ts +31 -0
- package/dist/types/jwk/thumbprint.d.ts +60 -0
- package/dist/types/jwks/local.d.ts +90 -0
- package/dist/types/jwks/remote.d.ts +306 -0
- package/dist/types/jws/compact/sign.d.ts +47 -0
- package/dist/types/jws/compact/verify.d.ts +45 -0
- package/dist/types/jws/flattened/sign.d.ts +53 -0
- package/dist/types/jws/flattened/verify.d.ts +50 -0
- package/dist/types/jws/general/sign.d.ts +67 -0
- package/dist/types/jws/general/verify.d.ts +61 -0
- package/dist/types/jwt/decrypt.d.ts +51 -0
- package/dist/types/jwt/encrypt.d.ts +105 -0
- package/dist/types/jwt/sign.d.ts +140 -0
- package/dist/types/jwt/unsecured.d.ts +70 -0
- package/dist/types/jwt/verify.d.ts +124 -0
- package/dist/types/key/export.d.ts +59 -0
- package/dist/types/key/generate_key_pair.d.ts +64 -0
- package/dist/types/key/generate_secret.d.ts +42 -0
- package/dist/types/key/import.d.ts +146 -0
- package/dist/types/types.d.ts +869 -0
- package/dist/types/util/base64url.d.ts +9 -0
- package/dist/types/util/decode_jwt.d.ts +25 -0
- package/dist/types/util/decode_protected_header.d.ts +24 -0
- package/dist/types/util/errors.d.ts +488 -0
- package/dist/webapi/index.js +32 -0
- package/dist/webapi/jwe/compact/decrypt.js +27 -0
- package/dist/webapi/jwe/compact/encrypt.js +27 -0
- package/dist/webapi/jwe/flattened/decrypt.js +159 -0
- package/dist/webapi/jwe/flattened/encrypt.js +167 -0
- package/dist/webapi/jwe/general/decrypt.js +31 -0
- package/dist/webapi/jwe/general/encrypt.js +182 -0
- package/dist/webapi/jwk/embedded.js +17 -0
- package/dist/webapi/jwk/thumbprint.js +68 -0
- package/dist/webapi/jwks/local.js +119 -0
- package/dist/webapi/jwks/remote.js +179 -0
- package/dist/webapi/jws/compact/sign.js +18 -0
- package/dist/webapi/jws/compact/verify.js +21 -0
- package/dist/webapi/jws/flattened/sign.js +87 -0
- package/dist/webapi/jws/flattened/verify.js +110 -0
- package/dist/webapi/jws/general/sign.js +70 -0
- package/dist/webapi/jws/general/verify.js +24 -0
- package/dist/webapi/jwt/decrypt.js +23 -0
- package/dist/webapi/jwt/encrypt.js +101 -0
- package/dist/webapi/jwt/sign.js +52 -0
- package/dist/webapi/jwt/unsecured.js +63 -0
- package/dist/webapi/jwt/verify.js +15 -0
- package/dist/webapi/key/export.js +11 -0
- package/dist/webapi/key/generate_key_pair.js +97 -0
- package/dist/webapi/key/generate_secret.js +40 -0
- package/dist/webapi/key/import.js +57 -0
- package/dist/webapi/lib/aesgcmkw.js +15 -0
- package/dist/webapi/lib/aeskw.js +25 -0
- package/dist/webapi/lib/asn1.js +243 -0
- package/dist/webapi/lib/base64.js +22 -0
- package/dist/webapi/lib/buffer_utils.js +43 -0
- package/dist/webapi/lib/check_key_type.js +127 -0
- package/dist/webapi/lib/content_encryption.js +217 -0
- package/dist/webapi/lib/crypto_key.js +136 -0
- package/dist/webapi/lib/deflate.js +44 -0
- package/dist/webapi/lib/ecdhes.js +52 -0
- package/dist/webapi/lib/helpers.js +19 -0
- package/dist/webapi/lib/invalid_key_input.js +27 -0
- package/dist/webapi/lib/is_key_like.js +17 -0
- package/dist/webapi/lib/jwk_to_key.js +107 -0
- package/dist/webapi/lib/jwt_claims_set.js +238 -0
- package/dist/webapi/lib/key_management.js +186 -0
- package/dist/webapi/lib/key_to_jwk.js +31 -0
- package/dist/webapi/lib/normalize_key.js +166 -0
- package/dist/webapi/lib/pbes2kw.js +42 -0
- package/dist/webapi/lib/rsaes.js +24 -0
- package/dist/webapi/lib/signing.js +74 -0
- package/dist/webapi/lib/type_checks.js +41 -0
- package/dist/webapi/lib/validate_algorithms.js +10 -0
- package/dist/webapi/lib/validate_crit.js +33 -0
- package/dist/webapi/util/base64url.js +30 -0
- package/dist/webapi/util/decode_jwt.js +32 -0
- package/dist/webapi/util/decode_protected_header.js +34 -0
- package/dist/webapi/util/errors.js +99 -0
- package/package.json +195 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { invalidKeyInput } from './invalid_key_input.js';
|
|
2
|
+
import { encodeBase64, decodeBase64 } from '../lib/base64.js';
|
|
3
|
+
import { JOSENotSupported } from '../util/errors.js';
|
|
4
|
+
import { isCryptoKey, isKeyObject } from './is_key_like.js';
|
|
5
|
+
const formatPEM = (b64, descriptor) => {
|
|
6
|
+
const newlined = (b64.match(/.{1,64}/g) || []).join('\n');
|
|
7
|
+
return `-----BEGIN ${descriptor}-----\n${newlined}\n-----END ${descriptor}-----`;
|
|
8
|
+
};
|
|
9
|
+
const genericExport = async (keyType, keyFormat, key) => {
|
|
10
|
+
if (isKeyObject(key)) {
|
|
11
|
+
if (key.type !== keyType) {
|
|
12
|
+
throw new TypeError(`key is not a ${keyType} key`);
|
|
13
|
+
}
|
|
14
|
+
return key.export({ format: 'pem', type: keyFormat });
|
|
15
|
+
}
|
|
16
|
+
if (!isCryptoKey(key)) {
|
|
17
|
+
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject'));
|
|
18
|
+
}
|
|
19
|
+
if (!key.extractable) {
|
|
20
|
+
throw new TypeError('CryptoKey is not extractable');
|
|
21
|
+
}
|
|
22
|
+
if (key.type !== keyType) {
|
|
23
|
+
throw new TypeError(`key is not a ${keyType} key`);
|
|
24
|
+
}
|
|
25
|
+
return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
|
|
26
|
+
};
|
|
27
|
+
export const toSPKI = (key) => genericExport('public', 'spki', key);
|
|
28
|
+
export const toPKCS8 = (key) => genericExport('private', 'pkcs8', key);
|
|
29
|
+
const bytesEqual = (a, b) => {
|
|
30
|
+
if (a.byteLength !== b.length)
|
|
31
|
+
return false;
|
|
32
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
33
|
+
if (a[i] !== b[i])
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
const createASN1State = (data) => ({ data, pos: 0 });
|
|
39
|
+
const parseLength = (state) => {
|
|
40
|
+
const first = state.data[state.pos++];
|
|
41
|
+
if (first & 0x80) {
|
|
42
|
+
const lengthOfLen = first & 0x7f;
|
|
43
|
+
let length = 0;
|
|
44
|
+
for (let i = 0; i < lengthOfLen; i++) {
|
|
45
|
+
length = (length << 8) | state.data[state.pos++];
|
|
46
|
+
}
|
|
47
|
+
return length;
|
|
48
|
+
}
|
|
49
|
+
return first;
|
|
50
|
+
};
|
|
51
|
+
const skipElement = (state, count = 1) => {
|
|
52
|
+
if (count <= 0)
|
|
53
|
+
return;
|
|
54
|
+
state.pos++;
|
|
55
|
+
const length = parseLength(state);
|
|
56
|
+
state.pos += length;
|
|
57
|
+
if (count > 1) {
|
|
58
|
+
skipElement(state, count - 1);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const expectTag = (state, expectedTag, errorMessage) => {
|
|
62
|
+
if (state.data[state.pos++] !== expectedTag) {
|
|
63
|
+
throw new Error(errorMessage);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const getSubarray = (state, length) => {
|
|
67
|
+
const result = state.data.subarray(state.pos, state.pos + length);
|
|
68
|
+
state.pos += length;
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
const parseAlgorithmOID = (state) => {
|
|
72
|
+
expectTag(state, 0x06, 'Expected algorithm OID');
|
|
73
|
+
const oidLen = parseLength(state);
|
|
74
|
+
return getSubarray(state, oidLen);
|
|
75
|
+
};
|
|
76
|
+
function parsePKCS8Header(state) {
|
|
77
|
+
expectTag(state, 0x30, 'Invalid PKCS#8 structure');
|
|
78
|
+
parseLength(state);
|
|
79
|
+
expectTag(state, 0x02, 'Expected version field');
|
|
80
|
+
const verLen = parseLength(state);
|
|
81
|
+
state.pos += verLen;
|
|
82
|
+
expectTag(state, 0x30, 'Expected algorithm identifier');
|
|
83
|
+
const algIdLen = parseLength(state);
|
|
84
|
+
const algIdStart = state.pos;
|
|
85
|
+
return { algIdStart, algIdLength: algIdLen };
|
|
86
|
+
}
|
|
87
|
+
function parseSPKIHeader(state) {
|
|
88
|
+
expectTag(state, 0x30, 'Invalid SPKI structure');
|
|
89
|
+
parseLength(state);
|
|
90
|
+
expectTag(state, 0x30, 'Expected algorithm identifier');
|
|
91
|
+
const algIdLen = parseLength(state);
|
|
92
|
+
const algIdStart = state.pos;
|
|
93
|
+
return { algIdStart, algIdLength: algIdLen };
|
|
94
|
+
}
|
|
95
|
+
const parseECAlgorithmIdentifier = (state) => {
|
|
96
|
+
const algOid = parseAlgorithmOID(state);
|
|
97
|
+
if (bytesEqual(algOid, [0x2b, 0x65, 0x6e])) {
|
|
98
|
+
return 'X25519';
|
|
99
|
+
}
|
|
100
|
+
if (!bytesEqual(algOid, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01])) {
|
|
101
|
+
throw new Error('Unsupported key algorithm');
|
|
102
|
+
}
|
|
103
|
+
expectTag(state, 0x06, 'Expected curve OID');
|
|
104
|
+
const curveOidLen = parseLength(state);
|
|
105
|
+
const curveOid = getSubarray(state, curveOidLen);
|
|
106
|
+
for (const { name, oid } of [
|
|
107
|
+
{ name: 'P-256', oid: [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07] },
|
|
108
|
+
{ name: 'P-384', oid: [0x2b, 0x81, 0x04, 0x00, 0x22] },
|
|
109
|
+
{ name: 'P-521', oid: [0x2b, 0x81, 0x04, 0x00, 0x23] },
|
|
110
|
+
]) {
|
|
111
|
+
if (bytesEqual(curveOid, oid)) {
|
|
112
|
+
return name;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Unsupported named curve');
|
|
116
|
+
};
|
|
117
|
+
const genericImport = async (keyFormat, keyData, alg, options) => {
|
|
118
|
+
let algorithm;
|
|
119
|
+
let keyUsages;
|
|
120
|
+
const isPublic = keyFormat === 'spki';
|
|
121
|
+
const getSigUsages = () => (isPublic ? ['verify'] : ['sign']);
|
|
122
|
+
const getEncUsages = () => isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
|
|
123
|
+
switch (alg) {
|
|
124
|
+
case 'PS256':
|
|
125
|
+
case 'PS384':
|
|
126
|
+
case 'PS512':
|
|
127
|
+
algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` };
|
|
128
|
+
keyUsages = getSigUsages();
|
|
129
|
+
break;
|
|
130
|
+
case 'RS256':
|
|
131
|
+
case 'RS384':
|
|
132
|
+
case 'RS512':
|
|
133
|
+
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` };
|
|
134
|
+
keyUsages = getSigUsages();
|
|
135
|
+
break;
|
|
136
|
+
case 'RSA-OAEP':
|
|
137
|
+
case 'RSA-OAEP-256':
|
|
138
|
+
case 'RSA-OAEP-384':
|
|
139
|
+
case 'RSA-OAEP-512':
|
|
140
|
+
algorithm = {
|
|
141
|
+
name: 'RSA-OAEP',
|
|
142
|
+
hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
|
|
143
|
+
};
|
|
144
|
+
keyUsages = getEncUsages();
|
|
145
|
+
break;
|
|
146
|
+
case 'ES256':
|
|
147
|
+
case 'ES384':
|
|
148
|
+
case 'ES512': {
|
|
149
|
+
const curveMap = { ES256: 'P-256', ES384: 'P-384', ES512: 'P-521' };
|
|
150
|
+
algorithm = { name: 'ECDSA', namedCurve: curveMap[alg] };
|
|
151
|
+
keyUsages = getSigUsages();
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'ECDH-ES':
|
|
155
|
+
case 'ECDH-ES+A128KW':
|
|
156
|
+
case 'ECDH-ES+A192KW':
|
|
157
|
+
case 'ECDH-ES+A256KW': {
|
|
158
|
+
try {
|
|
159
|
+
const namedCurve = options.getNamedCurve(keyData);
|
|
160
|
+
algorithm = namedCurve === 'X25519' ? { name: 'X25519' } : { name: 'ECDH', namedCurve };
|
|
161
|
+
}
|
|
162
|
+
catch (cause) {
|
|
163
|
+
throw new JOSENotSupported('Invalid or unsupported key format');
|
|
164
|
+
}
|
|
165
|
+
keyUsages = isPublic ? [] : ['deriveBits'];
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'Ed25519':
|
|
169
|
+
case 'EdDSA':
|
|
170
|
+
algorithm = { name: 'Ed25519' };
|
|
171
|
+
keyUsages = getSigUsages();
|
|
172
|
+
break;
|
|
173
|
+
case 'ML-DSA-44':
|
|
174
|
+
case 'ML-DSA-65':
|
|
175
|
+
case 'ML-DSA-87':
|
|
176
|
+
algorithm = { name: alg };
|
|
177
|
+
keyUsages = getSigUsages();
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value');
|
|
181
|
+
}
|
|
182
|
+
return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? (isPublic ? true : false), keyUsages);
|
|
183
|
+
};
|
|
184
|
+
const processPEMData = (pem, pattern) => {
|
|
185
|
+
return decodeBase64(pem.replace(pattern, ''));
|
|
186
|
+
};
|
|
187
|
+
export const fromPKCS8 = (pem, alg, options) => {
|
|
188
|
+
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g);
|
|
189
|
+
let opts = options;
|
|
190
|
+
if (alg?.startsWith?.('ECDH-ES')) {
|
|
191
|
+
opts ||= {};
|
|
192
|
+
opts.getNamedCurve = (keyData) => {
|
|
193
|
+
const state = createASN1State(keyData);
|
|
194
|
+
parsePKCS8Header(state);
|
|
195
|
+
return parseECAlgorithmIdentifier(state);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return genericImport('pkcs8', keyData, alg, opts);
|
|
199
|
+
};
|
|
200
|
+
export const fromSPKI = (pem, alg, options) => {
|
|
201
|
+
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g);
|
|
202
|
+
let opts = options;
|
|
203
|
+
if (alg?.startsWith?.('ECDH-ES')) {
|
|
204
|
+
opts ||= {};
|
|
205
|
+
opts.getNamedCurve = (keyData) => {
|
|
206
|
+
const state = createASN1State(keyData);
|
|
207
|
+
parseSPKIHeader(state);
|
|
208
|
+
return parseECAlgorithmIdentifier(state);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return genericImport('spki', keyData, alg, opts);
|
|
212
|
+
};
|
|
213
|
+
function spkiFromX509(buf) {
|
|
214
|
+
const state = createASN1State(buf);
|
|
215
|
+
expectTag(state, 0x30, 'Invalid certificate structure');
|
|
216
|
+
parseLength(state);
|
|
217
|
+
expectTag(state, 0x30, 'Invalid tbsCertificate structure');
|
|
218
|
+
parseLength(state);
|
|
219
|
+
if (buf[state.pos] === 0xa0) {
|
|
220
|
+
skipElement(state, 6);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
skipElement(state, 5);
|
|
224
|
+
}
|
|
225
|
+
const spkiStart = state.pos;
|
|
226
|
+
expectTag(state, 0x30, 'Invalid SPKI structure');
|
|
227
|
+
const spkiContentLen = parseLength(state);
|
|
228
|
+
return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart));
|
|
229
|
+
}
|
|
230
|
+
function extractX509SPKI(x509) {
|
|
231
|
+
const derBytes = processPEMData(x509, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g);
|
|
232
|
+
return spkiFromX509(derBytes);
|
|
233
|
+
}
|
|
234
|
+
export const fromX509 = (pem, alg, options) => {
|
|
235
|
+
let spki;
|
|
236
|
+
try {
|
|
237
|
+
spki = extractX509SPKI(pem);
|
|
238
|
+
}
|
|
239
|
+
catch (cause) {
|
|
240
|
+
throw new TypeError('Failed to parse the X.509 certificate', { cause });
|
|
241
|
+
}
|
|
242
|
+
return fromSPKI(formatPEM(encodeBase64(spki), 'PUBLIC KEY'), alg, options);
|
|
243
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function encodeBase64(input) {
|
|
2
|
+
if (Uint8Array.prototype.toBase64) {
|
|
3
|
+
return input.toBase64();
|
|
4
|
+
}
|
|
5
|
+
const CHUNK_SIZE = 0x8000;
|
|
6
|
+
const arr = [];
|
|
7
|
+
for (let i = 0; i < input.length; i += CHUNK_SIZE) {
|
|
8
|
+
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
9
|
+
}
|
|
10
|
+
return btoa(arr.join(''));
|
|
11
|
+
}
|
|
12
|
+
export function decodeBase64(encoded) {
|
|
13
|
+
if (Uint8Array.fromBase64) {
|
|
14
|
+
return Uint8Array.fromBase64(encoded);
|
|
15
|
+
}
|
|
16
|
+
const binary = atob(encoded);
|
|
17
|
+
const bytes = new Uint8Array(binary.length);
|
|
18
|
+
for (let i = 0; i < binary.length; i++) {
|
|
19
|
+
bytes[i] = binary.charCodeAt(i);
|
|
20
|
+
}
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const encoder = new TextEncoder();
|
|
2
|
+
export const decoder = new TextDecoder();
|
|
3
|
+
const MAX_INT32 = 2 ** 32;
|
|
4
|
+
export function concat(...buffers) {
|
|
5
|
+
const size = buffers.reduce((acc, { length }) => acc + length, 0);
|
|
6
|
+
const buf = new Uint8Array(size);
|
|
7
|
+
let i = 0;
|
|
8
|
+
for (const buffer of buffers) {
|
|
9
|
+
buf.set(buffer, i);
|
|
10
|
+
i += buffer.length;
|
|
11
|
+
}
|
|
12
|
+
return buf;
|
|
13
|
+
}
|
|
14
|
+
function writeUInt32BE(buf, value, offset) {
|
|
15
|
+
if (value < 0 || value >= MAX_INT32) {
|
|
16
|
+
throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
|
|
17
|
+
}
|
|
18
|
+
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);
|
|
19
|
+
}
|
|
20
|
+
export function uint64be(value) {
|
|
21
|
+
const high = Math.floor(value / MAX_INT32);
|
|
22
|
+
const low = value % MAX_INT32;
|
|
23
|
+
const buf = new Uint8Array(8);
|
|
24
|
+
writeUInt32BE(buf, high, 0);
|
|
25
|
+
writeUInt32BE(buf, low, 4);
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
28
|
+
export function uint32be(value) {
|
|
29
|
+
const buf = new Uint8Array(4);
|
|
30
|
+
writeUInt32BE(buf, value);
|
|
31
|
+
return buf;
|
|
32
|
+
}
|
|
33
|
+
export function encode(string) {
|
|
34
|
+
const bytes = new Uint8Array(string.length);
|
|
35
|
+
for (let i = 0; i < string.length; i++) {
|
|
36
|
+
const code = string.charCodeAt(i);
|
|
37
|
+
if (code > 127) {
|
|
38
|
+
throw new TypeError('non-ASCII string encountered in encode()');
|
|
39
|
+
}
|
|
40
|
+
bytes[i] = code;
|
|
41
|
+
}
|
|
42
|
+
return bytes;
|
|
43
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { withAlg as invalidKeyInput } from './invalid_key_input.js';
|
|
2
|
+
import { isKeyLike } from './is_key_like.js';
|
|
3
|
+
import * as jwk from './type_checks.js';
|
|
4
|
+
const tag = (key) => key?.[Symbol.toStringTag];
|
|
5
|
+
const jwkMatchesOp = (alg, key, usage) => {
|
|
6
|
+
if (key.use !== undefined) {
|
|
7
|
+
let expected;
|
|
8
|
+
switch (usage) {
|
|
9
|
+
case 'sign':
|
|
10
|
+
case 'verify':
|
|
11
|
+
expected = 'sig';
|
|
12
|
+
break;
|
|
13
|
+
case 'encrypt':
|
|
14
|
+
case 'decrypt':
|
|
15
|
+
expected = 'enc';
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
if (key.use !== expected) {
|
|
19
|
+
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (key.alg !== undefined && key.alg !== alg) {
|
|
23
|
+
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(key.key_ops)) {
|
|
26
|
+
let expectedKeyOp;
|
|
27
|
+
switch (true) {
|
|
28
|
+
case usage === 'sign' || usage === 'verify':
|
|
29
|
+
case alg === 'dir':
|
|
30
|
+
case alg.includes('CBC-HS'):
|
|
31
|
+
expectedKeyOp = usage;
|
|
32
|
+
break;
|
|
33
|
+
case alg.startsWith('PBES2'):
|
|
34
|
+
expectedKeyOp = 'deriveBits';
|
|
35
|
+
break;
|
|
36
|
+
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
|
|
37
|
+
if (!alg.includes('GCM') && alg.endsWith('KW')) {
|
|
38
|
+
expectedKeyOp = usage === 'encrypt' ? 'wrapKey' : 'unwrapKey';
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
expectedKeyOp = usage;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case usage === 'encrypt' && alg.startsWith('RSA'):
|
|
45
|
+
expectedKeyOp = 'wrapKey';
|
|
46
|
+
break;
|
|
47
|
+
case usage === 'decrypt':
|
|
48
|
+
expectedKeyOp = alg.startsWith('RSA') ? 'unwrapKey' : 'deriveBits';
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) {
|
|
52
|
+
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
const symmetricTypeCheck = (alg, key, usage) => {
|
|
58
|
+
if (key instanceof Uint8Array)
|
|
59
|
+
return;
|
|
60
|
+
if (jwk.isJWK(key)) {
|
|
61
|
+
if (jwk.isSecretJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
62
|
+
return;
|
|
63
|
+
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
|
|
64
|
+
}
|
|
65
|
+
if (!isKeyLike(key)) {
|
|
66
|
+
throw new TypeError(invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array'));
|
|
67
|
+
}
|
|
68
|
+
if (key.type !== 'secret') {
|
|
69
|
+
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const asymmetricTypeCheck = (alg, key, usage) => {
|
|
73
|
+
if (jwk.isJWK(key)) {
|
|
74
|
+
switch (usage) {
|
|
75
|
+
case 'decrypt':
|
|
76
|
+
if (jwk.isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
77
|
+
return;
|
|
78
|
+
throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
|
|
79
|
+
case 'sign':
|
|
80
|
+
if (jwk.isHardwareJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
81
|
+
return;
|
|
82
|
+
if (jwk.isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
83
|
+
return;
|
|
84
|
+
throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
|
|
85
|
+
case 'encrypt':
|
|
86
|
+
case 'verify':
|
|
87
|
+
if (jwk.isPublicJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
88
|
+
return;
|
|
89
|
+
throw new TypeError(`JSON Web Key for this operation must be a public JWK`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!isKeyLike(key)) {
|
|
93
|
+
throw new TypeError(invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
|
|
94
|
+
}
|
|
95
|
+
if (key.type === 'secret') {
|
|
96
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
|
|
97
|
+
}
|
|
98
|
+
if (key.type === 'public') {
|
|
99
|
+
switch (usage) {
|
|
100
|
+
case 'sign':
|
|
101
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
|
|
102
|
+
case 'decrypt':
|
|
103
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (key.type === 'private') {
|
|
107
|
+
switch (usage) {
|
|
108
|
+
case 'verify':
|
|
109
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
|
|
110
|
+
case 'encrypt':
|
|
111
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
export function checkKeyType(alg, key, usage) {
|
|
116
|
+
switch (alg.substring(0, 2)) {
|
|
117
|
+
case 'A1':
|
|
118
|
+
case 'A2':
|
|
119
|
+
case 'di':
|
|
120
|
+
case 'HS':
|
|
121
|
+
case 'PB':
|
|
122
|
+
symmetricTypeCheck(alg, key, usage);
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
asymmetricTypeCheck(alg, key, usage);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { concat, uint64be } from './buffer_utils.js';
|
|
2
|
+
import { checkEncCryptoKey } from './crypto_key.js';
|
|
3
|
+
import { invalidKeyInput } from './invalid_key_input.js';
|
|
4
|
+
import { JOSENotSupported, JWEDecryptionFailed, JWEInvalid } from '../util/errors.js';
|
|
5
|
+
import { isCryptoKey } from './is_key_like.js';
|
|
6
|
+
export function cekLength(alg) {
|
|
7
|
+
switch (alg) {
|
|
8
|
+
case 'A128GCM':
|
|
9
|
+
return 128;
|
|
10
|
+
case 'A192GCM':
|
|
11
|
+
return 192;
|
|
12
|
+
case 'A256GCM':
|
|
13
|
+
case 'A128CBC-HS256':
|
|
14
|
+
return 256;
|
|
15
|
+
case 'A192CBC-HS384':
|
|
16
|
+
return 384;
|
|
17
|
+
case 'A256CBC-HS512':
|
|
18
|
+
return 512;
|
|
19
|
+
default:
|
|
20
|
+
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const generateCek = (alg) => crypto.getRandomValues(new Uint8Array(cekLength(alg) >> 3));
|
|
24
|
+
function checkCekLength(cek, expected) {
|
|
25
|
+
const actual = cek.byteLength << 3;
|
|
26
|
+
if (actual !== expected) {
|
|
27
|
+
throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function ivBitLength(alg) {
|
|
31
|
+
switch (alg) {
|
|
32
|
+
case 'A128GCM':
|
|
33
|
+
case 'A128GCMKW':
|
|
34
|
+
case 'A192GCM':
|
|
35
|
+
case 'A192GCMKW':
|
|
36
|
+
case 'A256GCM':
|
|
37
|
+
case 'A256GCMKW':
|
|
38
|
+
return 96;
|
|
39
|
+
case 'A128CBC-HS256':
|
|
40
|
+
case 'A192CBC-HS384':
|
|
41
|
+
case 'A256CBC-HS512':
|
|
42
|
+
return 128;
|
|
43
|
+
default:
|
|
44
|
+
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const generateIv = (alg) => crypto.getRandomValues(new Uint8Array(ivBitLength(alg) >> 3));
|
|
48
|
+
export function checkIvLength(enc, iv) {
|
|
49
|
+
if (iv.length << 3 !== ivBitLength(enc)) {
|
|
50
|
+
throw new JWEInvalid('Invalid Initialization Vector length');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function cbcKeySetup(enc, cek, usage) {
|
|
54
|
+
if (!(cek instanceof Uint8Array)) {
|
|
55
|
+
throw new TypeError(invalidKeyInput(cek, 'Uint8Array'));
|
|
56
|
+
}
|
|
57
|
+
const keySize = parseInt(enc.slice(1, 4), 10);
|
|
58
|
+
const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, [usage]);
|
|
59
|
+
const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), {
|
|
60
|
+
hash: `SHA-${keySize << 1}`,
|
|
61
|
+
name: 'HMAC',
|
|
62
|
+
}, false, ['sign']);
|
|
63
|
+
return { encKey, macKey, keySize };
|
|
64
|
+
}
|
|
65
|
+
async function cbcHmacTag(macKey, macData, keySize) {
|
|
66
|
+
return new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3));
|
|
67
|
+
}
|
|
68
|
+
async function cbcEncrypt(enc, plaintext, cek, iv, aad) {
|
|
69
|
+
const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'encrypt');
|
|
70
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
71
|
+
iv: iv,
|
|
72
|
+
name: 'AES-CBC',
|
|
73
|
+
}, encKey, plaintext));
|
|
74
|
+
const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3));
|
|
75
|
+
const tag = await cbcHmacTag(macKey, macData, keySize);
|
|
76
|
+
return { ciphertext, tag, iv };
|
|
77
|
+
}
|
|
78
|
+
async function timingSafeEqual(a, b) {
|
|
79
|
+
if (!(a instanceof Uint8Array)) {
|
|
80
|
+
throw new TypeError('First argument must be a buffer');
|
|
81
|
+
}
|
|
82
|
+
if (!(b instanceof Uint8Array)) {
|
|
83
|
+
throw new TypeError('Second argument must be a buffer');
|
|
84
|
+
}
|
|
85
|
+
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
|
|
86
|
+
const key = (await crypto.subtle.generateKey(algorithm, false, ['sign']));
|
|
87
|
+
const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a));
|
|
88
|
+
const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b));
|
|
89
|
+
let out = 0;
|
|
90
|
+
let i = -1;
|
|
91
|
+
while (++i < 32) {
|
|
92
|
+
out |= aHmac[i] ^ bHmac[i];
|
|
93
|
+
}
|
|
94
|
+
return out === 0;
|
|
95
|
+
}
|
|
96
|
+
async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
97
|
+
const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'decrypt');
|
|
98
|
+
const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3));
|
|
99
|
+
const expectedTag = await cbcHmacTag(macKey, macData, keySize);
|
|
100
|
+
let macCheckPassed;
|
|
101
|
+
try {
|
|
102
|
+
macCheckPassed = await timingSafeEqual(tag, expectedTag);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
}
|
|
106
|
+
if (!macCheckPassed) {
|
|
107
|
+
throw new JWEDecryptionFailed();
|
|
108
|
+
}
|
|
109
|
+
let plaintext;
|
|
110
|
+
try {
|
|
111
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({ iv: iv, name: 'AES-CBC' }, encKey, ciphertext));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
}
|
|
115
|
+
if (!plaintext) {
|
|
116
|
+
throw new JWEDecryptionFailed();
|
|
117
|
+
}
|
|
118
|
+
return plaintext;
|
|
119
|
+
}
|
|
120
|
+
async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
|
121
|
+
let encKey;
|
|
122
|
+
if (cek instanceof Uint8Array) {
|
|
123
|
+
encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
checkEncCryptoKey(cek, enc, 'encrypt');
|
|
127
|
+
encKey = cek;
|
|
128
|
+
}
|
|
129
|
+
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
130
|
+
additionalData: aad,
|
|
131
|
+
iv: iv,
|
|
132
|
+
name: 'AES-GCM',
|
|
133
|
+
tagLength: 128,
|
|
134
|
+
}, encKey, plaintext));
|
|
135
|
+
const tag = encrypted.slice(-16);
|
|
136
|
+
const ciphertext = encrypted.slice(0, -16);
|
|
137
|
+
return { ciphertext, tag, iv };
|
|
138
|
+
}
|
|
139
|
+
async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
140
|
+
let encKey;
|
|
141
|
+
if (cek instanceof Uint8Array) {
|
|
142
|
+
encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['decrypt']);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
checkEncCryptoKey(cek, enc, 'decrypt');
|
|
146
|
+
encKey = cek;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return new Uint8Array(await crypto.subtle.decrypt({
|
|
150
|
+
additionalData: aad,
|
|
151
|
+
iv: iv,
|
|
152
|
+
name: 'AES-GCM',
|
|
153
|
+
tagLength: 128,
|
|
154
|
+
}, encKey, concat(ciphertext, tag)));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new JWEDecryptionFailed();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const unsupportedEnc = 'Unsupported JWE Content Encryption Algorithm';
|
|
161
|
+
export async function encrypt(enc, plaintext, cek, iv, aad) {
|
|
162
|
+
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
|
|
163
|
+
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'KeyObject', 'Uint8Array', 'JSON Web Key'));
|
|
164
|
+
}
|
|
165
|
+
if (iv) {
|
|
166
|
+
checkIvLength(enc, iv);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
iv = generateIv(enc);
|
|
170
|
+
}
|
|
171
|
+
switch (enc) {
|
|
172
|
+
case 'A128CBC-HS256':
|
|
173
|
+
case 'A192CBC-HS384':
|
|
174
|
+
case 'A256CBC-HS512':
|
|
175
|
+
if (cek instanceof Uint8Array) {
|
|
176
|
+
checkCekLength(cek, parseInt(enc.slice(-3), 10));
|
|
177
|
+
}
|
|
178
|
+
return cbcEncrypt(enc, plaintext, cek, iv, aad);
|
|
179
|
+
case 'A128GCM':
|
|
180
|
+
case 'A192GCM':
|
|
181
|
+
case 'A256GCM':
|
|
182
|
+
if (cek instanceof Uint8Array) {
|
|
183
|
+
checkCekLength(cek, parseInt(enc.slice(1, 4), 10));
|
|
184
|
+
}
|
|
185
|
+
return gcmEncrypt(enc, plaintext, cek, iv, aad);
|
|
186
|
+
default:
|
|
187
|
+
throw new JOSENotSupported(unsupportedEnc);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function decrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
191
|
+
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
|
|
192
|
+
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'KeyObject', 'Uint8Array', 'JSON Web Key'));
|
|
193
|
+
}
|
|
194
|
+
if (!iv) {
|
|
195
|
+
throw new JWEInvalid('JWE Initialization Vector missing');
|
|
196
|
+
}
|
|
197
|
+
if (!tag) {
|
|
198
|
+
throw new JWEInvalid('JWE Authentication Tag missing');
|
|
199
|
+
}
|
|
200
|
+
checkIvLength(enc, iv);
|
|
201
|
+
switch (enc) {
|
|
202
|
+
case 'A128CBC-HS256':
|
|
203
|
+
case 'A192CBC-HS384':
|
|
204
|
+
case 'A256CBC-HS512':
|
|
205
|
+
if (cek instanceof Uint8Array)
|
|
206
|
+
checkCekLength(cek, parseInt(enc.slice(-3), 10));
|
|
207
|
+
return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad);
|
|
208
|
+
case 'A128GCM':
|
|
209
|
+
case 'A192GCM':
|
|
210
|
+
case 'A256GCM':
|
|
211
|
+
if (cek instanceof Uint8Array)
|
|
212
|
+
checkCekLength(cek, parseInt(enc.slice(1, 4), 10));
|
|
213
|
+
return gcmDecrypt(enc, cek, ciphertext, iv, tag, aad);
|
|
214
|
+
default:
|
|
215
|
+
throw new JOSENotSupported(unsupportedEnc);
|
|
216
|
+
}
|
|
217
|
+
}
|