forked from element-hq/element-web
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMegolmExportEncryption.ts
More file actions
352 lines (306 loc) · 10.9 KB
/
Copy pathMegolmExportEncryption.ts
File metadata and controls
352 lines (306 loc) · 10.9 KB
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../languageHandler";
import SdkConfig from "../SdkConfig";
const subtleCrypto = window.crypto.subtle;
/**
* Make an Error object which has a friendlyText property which is already
* translated and suitable for showing to the user.
*
* @param {string} message message for the exception
* @param {string} friendlyText
* @returns {{message: string, friendlyText: string}}
*/
function friendlyError(message: string, friendlyText: string): { message: string; friendlyText: string } {
return { message, friendlyText };
}
function cryptoFailMsg(): string {
return _t("encryption|export_unsupported");
}
/**
* Decrypt a megolm key file
*
* @param {ArrayBuffer} data file to decrypt
* @param {String} password
* @return {Promise<String>} promise for decrypted output
*
*
*/
export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string): Promise<string> {
const body = unpackMegolmKeyFile(data);
const brand = SdkConfig.get().brand;
// check we have a version byte
if (body.length < 1) {
throw friendlyError("Invalid file: too short", _t("encryption|import_invalid_keyfile", { brand }));
}
const version = body[0];
if (version !== 1) {
throw friendlyError("Unsupported version", _t("encryption|import_invalid_keyfile", { brand }));
}
const ciphertextLength = body.length - (1 + 16 + 16 + 4 + 32);
if (ciphertextLength < 0) {
throw friendlyError("Invalid file: too short", _t("encryption|import_invalid_keyfile", { brand }));
}
const salt = body.subarray(1, 1 + 16);
const iv = body.subarray(17, 17 + 16);
const iterations = (body[33] << 24) | (body[34] << 16) | (body[35] << 8) | body[36];
const ciphertext = body.subarray(37, 37 + ciphertextLength);
const hmac = body.subarray(-32);
const [aesKey, hmacKey] = await deriveKeys(salt, iterations, password);
const toVerify = body.subarray(0, -32);
let isValid;
try {
isValid = await subtleCrypto.verify({ name: "HMAC" }, hmacKey, hmac, toVerify);
} catch (e) {
throw friendlyError("subtleCrypto.verify failed: " + e, cryptoFailMsg());
}
if (!isValid) {
throw friendlyError("hmac mismatch", _t("encryption|import_invalid_passphrase"));
}
let plaintext;
try {
plaintext = await subtleCrypto.decrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
ciphertext,
);
} catch (e) {
throw friendlyError("subtleCrypto.decrypt failed: " + e, cryptoFailMsg());
}
return new TextDecoder().decode(new Uint8Array(plaintext));
}
/**
* Encrypt a megolm key file
*
* @param {String} data
* @param {String} password
* @param {Object=} options
* @param {Number=} options.kdf_rounds Number of iterations to perform of the
* key-derivation function.
* @return {Promise<ArrayBuffer>} promise for encrypted output
*/
export async function encryptMegolmKeyFile(
data: string,
password: string,
options?: { kdf_rounds?: number }, // eslint-disable-line camelcase
): Promise<ArrayBuffer> {
options = options || {};
const kdfRounds = options.kdf_rounds || 500000;
const salt = new Uint8Array(16);
window.crypto.getRandomValues(salt);
const iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
const [aesKey, hmacKey] = await deriveKeys(salt, kdfRounds, password);
const encodedData = new TextEncoder().encode(data);
let ciphertext;
try {
ciphertext = await subtleCrypto.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
} catch (e) {
throw friendlyError("subtleCrypto.encrypt failed: " + e, cryptoFailMsg());
}
const cipherArray = new Uint8Array(ciphertext);
const bodyLength = 1 + salt.length + iv.length + 4 + cipherArray.length + 32;
const resultBuffer = new Uint8Array(bodyLength);
let idx = 0;
resultBuffer[idx++] = 1; // version
resultBuffer.set(salt, idx);
idx += salt.length;
resultBuffer.set(iv, idx);
idx += iv.length;
resultBuffer[idx++] = kdfRounds >> 24;
resultBuffer[idx++] = (kdfRounds >> 16) & 0xff;
resultBuffer[idx++] = (kdfRounds >> 8) & 0xff;
resultBuffer[idx++] = kdfRounds & 0xff;
resultBuffer.set(cipherArray, idx);
idx += cipherArray.length;
const toSign = resultBuffer.subarray(0, idx);
let hmac;
try {
hmac = await subtleCrypto.sign({ name: "HMAC" }, hmacKey, toSign);
} catch (e) {
throw friendlyError("subtleCrypto.sign failed: " + e, cryptoFailMsg());
}
const hmacArray = new Uint8Array(hmac);
resultBuffer.set(hmacArray, idx);
return packMegolmKeyFile(resultBuffer);
}
/**
* Derive the AES and HMAC-SHA-256 keys for the file
*
* @param {Unit8Array} salt salt for pbkdf
* @param {Number} iterations number of pbkdf iterations
* @param {String} password password
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
*/
async function deriveKeys(salt: Uint8Array, iterations: number, password: string): Promise<[CryptoKey, CryptoKey]> {
const start = new Date();
let key;
try {
key = await subtleCrypto.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, [
"deriveBits",
]);
} catch (e) {
throw friendlyError("subtleCrypto.importKey failed: " + e, cryptoFailMsg());
}
let keybits;
try {
keybits = await subtleCrypto.deriveBits(
{
name: "PBKDF2",
salt: salt,
iterations: iterations,
hash: "SHA-512",
},
key,
512,
);
} catch (e) {
throw friendlyError("subtleCrypto.deriveBits failed: " + e, cryptoFailMsg());
}
const now = new Date();
logger.log("E2e import/export: deriveKeys took " + (now.getTime() - start.getTime()) + "ms");
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = subtleCrypto
.importKey("raw", aesKey, { name: "AES-CTR" }, false, ["encrypt", "decrypt"])
.catch((e) => {
throw friendlyError("subtleCrypto.importKey failed for AES key: " + e, cryptoFailMsg());
});
const hmacProm = subtleCrypto
.importKey(
"raw",
hmacKey,
{
name: "HMAC",
hash: { name: "SHA-256" },
},
false,
["sign", "verify"],
)
.catch((e) => {
throw friendlyError("subtleCrypto.importKey failed for HMAC key: " + e, cryptoFailMsg());
});
return Promise.all([aesProm, hmacProm]);
}
const HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----";
const TRAILER_LINE = "-----END MEGOLM SESSION DATA-----";
/**
* Unbase64 an ascii-armoured megolm key file
*
* Strips the header and trailer lines, and unbase64s the content
*
* @param {ArrayBuffer} data input file
* @return {Uint8Array} unbase64ed content
*/
function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array {
// parse the file as a great big String. This should be safe, because there
// should be no non-ASCII characters, and it means that we can do string
// comparisons to find the header and footer, and feed it into window.atob.
const fileStr = new TextDecoder().decode(new Uint8Array(data));
// look for the start line
let lineStart = 0;
// eslint-disable-next-line no-constant-condition
while (1) {
const lineEnd = fileStr.indexOf("\n", lineStart);
if (lineEnd < 0) {
throw new Error("Header line not found");
}
const line = fileStr.slice(lineStart, lineEnd).trim();
// start the next line after the newline
lineStart = lineEnd + 1;
if (line === HEADER_LINE) {
break;
}
}
const dataStart = lineStart;
// look for the end line
// eslint-disable-next-line no-constant-condition
while (1) {
const lineEnd = fileStr.indexOf("\n", lineStart);
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim();
if (line === TRAILER_LINE) {
break;
}
if (lineEnd < 0) {
throw new Error("Trailer line not found");
}
// start the next line after the newline
lineStart = lineEnd + 1;
}
const dataEnd = lineStart;
return decodeBase64(fileStr.slice(dataStart, dataEnd));
}
/**
* ascii-armour a megolm key file
*
* base64s the content, and adds header and trailer lines
*
* @param {Uint8Array} data raw data
* @return {ArrayBuffer} formatted file
*/
function packMegolmKeyFile(data: Uint8Array): ArrayBuffer {
// we split into lines before base64ing, because encodeBase64 doesn't deal
// terribly well with large arrays.
const LINE_LENGTH = (72 * 4) / 3;
const nLines = Math.ceil(data.length / LINE_LENGTH);
const lines = new Array(nLines + 3);
lines[0] = HEADER_LINE;
let o = 0;
let i;
for (i = 1; i <= nLines; i++) {
lines[i] = encodeBase64(data.subarray(o, o + LINE_LENGTH));
o += LINE_LENGTH;
}
lines[i++] = TRAILER_LINE;
lines[i] = "";
return new TextEncoder().encode(lines.join("\n")).buffer;
}
/**
* Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64.
*/
function encodeBase64(uint8Array: Uint8Array): string {
// Misinterpt the Uint8Array as Latin-1.
// window.btoa expects a unicode string with codepoints in the range 0-255.
const latin1String = String.fromCharCode.apply(null, Array.from(uint8Array));
// Use the builtin base64 encoder.
return window.btoa(latin1String);
}
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data.
*/
function decodeBase64(base64: string): Uint8Array {
// window.atob returns a unicode string with codepoints in the range 0-255.
const latin1String = window.atob(base64);
// Encode the string as a Uint8Array
const uint8Array = new Uint8Array(latin1String.length);
for (let i = 0; i < latin1String.length; i++) {
uint8Array[i] = latin1String.charCodeAt(i);
}
return uint8Array;
}