1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248import forge from 'node-forge';
export interface AESMessage {
iv: string;
t: string;
d: string;
ad: string;
}
/**
* Encrypt with RSA256 public key
*
* @param publicKeyJWK
* @param plaintext
* @return String
*/
export function encryptRSAWithJWK(publicKeyJWK: JsonWebKey, plaintext: string) {
if (publicKeyJWK.alg !== 'RSA-OAEP-256') {
throw new Error('Public key algorithm was not RSA-OAEP-256');
} else if (publicKeyJWK.kty !== 'RSA') {
throw new Error('Public key type was not RSA');
} else if (!publicKeyJWK.key_ops || !publicKeyJWK.key_ops.find(o => o === 'encrypt')) {
throw new Error('Public key does not have "encrypt" op');
} else if (!publicKeyJWK.n || !publicKeyJWK.e) {
throw new Error('Public key is missing parameters');
}
const encodedPlaintext = encodeURIComponent(plaintext);
const n = _b64UrlToBigInt(publicKeyJWK.n);
const e = _b64UrlToBigInt(publicKeyJWK.e);
// @ts-expect-error -- TSCONVERSION appears not to be exported for some reason
const publicKey = forge.rsa.setPublicKey(n, e);
const encrypted = publicKey.encrypt(encodedPlaintext, 'RSA-OAEP', {
md: forge.md.sha256.create(),
});
return forge.util.bytesToHex(encrypted);
}
export function decryptRSAWithJWK(privateJWK: JsonWebKey, encryptedBlob: string) {
if (
!privateJWK.n ||
!privateJWK.e ||
!privateJWK.d ||
!privateJWK.p ||
!privateJWK.q ||
!privateJWK.dp ||
!privateJWK.dq ||
!privateJWK.qi
) {
throw new Error('Private key is missing parameters');
}
const n = _b64UrlToBigInt(privateJWK.n);
const e = _b64UrlToBigInt(privateJWK.e);
const d = _b64UrlToBigInt(privateJWK.d);
const p = _b64UrlToBigInt(privateJWK.p);
const q = _b64UrlToBigInt(privateJWK.q);
const dP = _b64UrlToBigInt(privateJWK.dp);
const dQ = _b64UrlToBigInt(privateJWK.dq);
const qInv = _b64UrlToBigInt(privateJWK.qi);
// @ts-expect-error -- TSCONVERSION appears not to be exported for some reason
const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv);
const bytes = forge.util.hexToBytes(encryptedBlob);
const decrypted = privateKey.decrypt(bytes, 'RSA-OAEP', {
md: forge.md.sha256.create(),
});
return decodeURIComponent(decrypted);
}
/**
* Encrypt data using symmetric key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param buff data to encrypt
* @param additionalData any additional public data to attach
* @returns {{iv, t, d, ad}}
*/
export function encryptAESBuffer(jwkOrKey: string | JsonWebKey, buff: Buffer, additionalData = ''): AESMessage {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || '');
const key = forge.util.hexToBytes(rawKey);
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
cipher.start({
additionalData,
iv,
tagLength: 128,
});
cipher.update(forge.util.createBuffer(buff));
cipher.finish();
return {
iv: forge.util.bytesToHex(iv),
// @ts-expect-error -- TSCONVERSION needs to be converted to string
t: forge.util.bytesToHex(cipher.mode.tag),
ad: forge.util.bytesToHex(additionalData),
// @ts-expect-error -- TSCONVERSION needs to be converted to string
d: forge.util.bytesToHex(cipher.output),
};
}
/**
* Encrypt data using symmetric key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param plaintext string of data to encrypt
* @param additionalData any additional public data to attach
* @returns {{iv, t, d, ad}}
*/
export function encryptAES(jwkOrKey: string | JsonWebKey, plaintext: string, additionalData = ''): AESMessage {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || '');
const key = forge.util.hexToBytes(rawKey);
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
// Plaintext could contain weird unicode, so we have to encode that
const encodedPlaintext = encodeURIComponent(plaintext);
cipher.start({
additionalData,
iv,
tagLength: 128,
});
cipher.update(forge.util.createBuffer(encodedPlaintext));
cipher.finish();
return {
iv: forge.util.bytesToHex(iv),
// @ts-expect-error -- TSCONVERSION needs to be converted to string
t: forge.util.bytesToHex(cipher.mode.tag),
ad: forge.util.bytesToHex(additionalData),
// @ts-expect-error -- TSCONVERSION needs to be converted to string
d: forge.util.bytesToHex(cipher.output),
};
}
/**
* Decrypt AES using a key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param encryptedResult encryption data
* @returns String
*/
export function decryptAES(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || '');
const key = forge.util.hexToBytes(rawKey);
// ~~~~~~~~~~~~~~~~~~~~ //
// Decrypt with AES-GCM //
// ~~~~~~~~~~~~~~~~~~~~ //
const decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({
iv: forge.util.hexToBytes(encryptedResult.iv),
tagLength: encryptedResult.t.length * 4,
// @ts-expect-error -- TSCONVERSION needs to be converted to string
tag: forge.util.hexToBytes(encryptedResult.t),
additionalData: forge.util.hexToBytes(encryptedResult.ad),
});
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
if (decipher.finish()) {
return decodeURIComponent(decipher.output.toString());
}
throw new Error('Failed to decrypt data');
}
/**
* Decrypts AES using a key to buffer
* @param jwkOrKey
* @param encryptedResult
* @returns {string}
*/
export function decryptAESToBuffer(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || '');
const key = forge.util.hexToBytes(rawKey);
// ~~~~~~~~~~~~~~~~~~~~ //
// Decrypt with AES-GCM //
// ~~~~~~~~~~~~~~~~~~~~ //
const decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({
iv: forge.util.hexToBytes(encryptedResult.iv),
tagLength: encryptedResult.t.length * 4,
// @ts-expect-error -- TSCONVERSION needs to be converted to string
tag: forge.util.hexToBytes(encryptedResult.t),
additionalData: forge.util.hexToBytes(encryptedResult.ad),
});
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
if (decipher.finish()) {
// @ts-expect-error -- TSCONVERSION needs to be converted to string
return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex');
}
throw new Error('Failed to decrypt data');
}
/**
* Generate a random AES256 key for use with symmetric encryption
*/
export async function generateAES256Key() {
const c = window.crypto;
// @ts-expect-error -- TSCONVERSION: likely needs a module augmentation for webkit
const subtle = c ? c.subtle || c.webkitSubtle : null;
if (subtle) {
console.log('[crypt] Using Native AES Key Generation');
const key = await subtle.generateKey(
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt', 'decrypt'],
);
return subtle.exportKey('jwk', key);
}
console.log('[crypt] Using Fallback Forge AES Key Generation');
const key = forge.util.bytesToHex(forge.random.getBytesSync(32));
return {
kty: 'oct',
alg: 'A256GCM',
ext: true,
key_ops: ['encrypt', 'decrypt'],
k: _hexToB64Url(key),
};
}
// ~~~~~~~~~~~~~~~~ //
// Helper Functions //
// ~~~~~~~~~~~~~~~~ //
function _hexToB64Url(h: string) {
const bytes = forge.util.hexToBytes(h);
return window.btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
function _b64UrlToBigInt(s: string) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- see below
// @ts-ignore -- unfortunately, we must ignore here instead of the usual expect-error because this mondule is being used by two different builds (`insomnia` and `insomnia-send-request`) and in one of them this line is an error (`insomnia-send-request`) and the other it is not ()`insomnia`).
return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16);
}
function _b64UrlToHex(s: string) {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
return forge.util.bytesToHex(window.atob(b64));
}