add support to decrypt e2e keys
authorunknown <ajbura@gmail.com>
Sun, 1 Aug 2021 13:58:09 +0000 (19:28 +0530)
committerunknown <ajbura@gmail.com>
Sun, 1 Aug 2021 13:58:09 +0000 (19:28 +0530)
src/util/decryptE2ERoomKeys.js [new file with mode: 0644]

diff --git a/src/util/decryptE2ERoomKeys.js b/src/util/decryptE2ERoomKeys.js
new file mode 100644 (file)
index 0000000..2b00872
--- /dev/null
@@ -0,0 +1,225 @@
+// https://github.com/matrix-org/matrix-react-sdk/blob/e78a1adb6f1af2ea425b0bae9034fb7344a4b2e8/src/utils/MegolmExportEncryption.js
+
+const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
+
+/**
+ * Make an Error object which has a friendlyText property which is already
+ * translated and suitable for showing to the user.
+ *
+ * @param {string} msg   message for the exception
+ * @param {string} friendlyText
+ * @returns {Error}
+ */
+function friendlyError(msg, friendlyText) {
+  const e = new Error(msg);
+  e.friendlyText = friendlyText;
+  return e;
+}
+
+function cryptoFailMsg() {
+  return 'Your browser does not support the required cryptography extensions';
+}
+/**
+ * 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, iterations, password) {
+  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,
+        iterations,
+        hash: 'SHA-512',
+      },
+      key,
+      512,
+    );
+  } catch (e) {
+    throw friendlyError(`subtleCrypto.deriveBits failed: ${e}`, cryptoFailMsg());
+  }
+
+  const now = new Date();
+  console.log(`E2e import/export: deriveKeys took ${(now - start)}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());
+  });
+
+  // eslint-disable-next-line no-return-await
+  return await Promise.all([aesProm, hmacProm]);
+}
+
+/**
+ * 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) {
+  // 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 += 1) {
+    uint8Array[i] = latin1String.charCodeAt(i);
+  }
+  return uint8Array;
+}
+
+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) {
+  // 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;
+  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
+  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));
+}
+
+export default async function decryptMegolmKeyFile(data, password) {
+  const body = unpackMegolmKeyFile(data);
+
+  // check we have a version byte
+  if (body.length < 1) {
+    throw friendlyError('Invalid file: too short', 'Not a valid keyfile');
+  }
+
+  const version = body[0];
+  if (version !== 1) {
+    throw friendlyError('Unsupported version', 'Not a valid keyfile');
+  }
+
+  const ciphertextLength = body.length - (1 + 16 + 16 + 4 + 32);
+  if (ciphertextLength < 0) {
+    throw friendlyError('Invalid file: too short', 'Not a valid keyfile');
+  }
+
+  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', 'Authentication check failed: Incorrect password?');
+  }
+
+  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));
+}