From 00eade1c16b58599430500cfd9d8492e9f8015d5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Sep 2020 10:19:09 +0200 Subject: [PATCH] add crypto driver with primitives needed for 4S & session backup --- src/legacy-extras.js | 6 +- src/main.js | 2 + src/matrix/Session.js | 2 +- src/matrix/SessionContainer.js | 5 +- src/ui/web/dom/CryptoDriver.js | 257 +++++++++++++++++++++++++++++++++ src/utils/crypto/hkdf.js | 29 ++++ src/utils/crypto/pbkdf2.js | 65 +++++++++ 7 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 src/ui/web/dom/CryptoDriver.js create mode 100644 src/utils/crypto/hkdf.js create mode 100644 src/utils/crypto/pbkdf2.js diff --git a/src/legacy-extras.js b/src/legacy-extras.js index 43fe5eb6..e0767acd 100644 --- a/src/legacy-extras.js +++ b/src/legacy-extras.js @@ -1,2 +1,6 @@ import aesjs from "../lib/aes-js/index.js"; -export const legacyExtras = {aesjs}; +import hkdf from "./utils/crypto/hkdf.js"; + +// these are run-time dependencies that are only needed for the legacy bundle. +// they are exported here and passed into main to make them available to the app. +export const legacyExtras = {crypto:{aesjs, hkdf}}; diff --git a/src/main.js b/src/main.js index 3c99f04f..e6e69bdd 100644 --- a/src/main.js +++ b/src/main.js @@ -25,6 +25,7 @@ import {BrawlViewModel} from "./domain/BrawlViewModel.js"; import {BrawlView} from "./ui/web/BrawlView.js"; import {Clock} from "./ui/web/dom/Clock.js"; import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js"; +import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js"; import {WorkerPool} from "./utils/WorkerPool.js"; import {OlmWorker} from "./matrix/e2ee/OlmWorker.js"; @@ -122,6 +123,7 @@ export async function main(container, paths, legacyExtras) { sessionInfoStorage, request, clock, + cryptoDriver: new CryptoDriver(legacyExtras?.crypto), olmPromise, workerPromise, }); diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f8ac2f40..75840790 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -34,7 +34,7 @@ const PICKLE_KEY = "DEFAULT_KEY"; export class Session { // sessionInfo contains deviceId, userId and homeServer - constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker}) { + constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker, cryptoDriver}) { this._clock = clock; this._storage = storage; this._hsApi = hsApi; diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 98ffd2b1..458e4c06 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -42,7 +42,7 @@ export const LoginFailure = createEnum( ); export class SessionContainer { - constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise}) { + constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise, cryptoDriver}) { this._random = random; this._clock = clock; this._onlineStatus = onlineStatus; @@ -60,6 +60,7 @@ export class SessionContainer { this._storage = null; this._olmPromise = olmPromise; this._workerPromise = workerPromise; + this._cryptoDriver = cryptoDriver; } createNewSessionId() { @@ -159,7 +160,7 @@ export class SessionContainer { } this._session = new Session({storage: this._storage, sessionInfo: filteredSessionInfo, hsApi, olm, - clock: this._clock, olmWorker}); + clock: this._clock, olmWorker, cryptoDriver: this._cryptoDriver}); await this._session.load(); this._status.set(LoadStatus.SessionSetup); await this._session.beforeFirstSync(isNewLogin); diff --git a/src/ui/web/dom/CryptoDriver.js b/src/ui/web/dom/CryptoDriver.js new file mode 100644 index 00000000..1f856000 --- /dev/null +++ b/src/ui/web/dom/CryptoDriver.js @@ -0,0 +1,257 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// turn IE11 result into promise +function subtleCryptoResult(promiseOrOp, method) { + if (promiseOrOp instanceof Promise) { + return promiseOrOp; + } else { + return new Promise((resolve, reject) => { + promiseOrOp.oncomplete = e => resolve(e.target.result); + promiseOrOp.onerror = () => reject(new Error("Crypto error on " + method)); + }); + } +} + +class CryptoHMACDriver { + constructor(subtleCrypto) { + this._subtleCrypto = subtleCrypto; + } + /** + * [hmac description] + * @param {BufferSource} key + * @param {BufferSource} mac + * @param {BufferSource} data + * @param {HashName} hash + * @return {boolean} + */ + async verify(key, mac, data, hash) { + const opts = { + name: 'HMAC', + hash: {name: hashName(hash)}, + }; + const hmacKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + opts, + false, + ['verify'], + ), "importKey"); + const isVerified = await subtleCryptoResult(this._subtleCrypto.verify( + opts, + hmacKey, + mac, + data, + ), "verify"); + return isVerified; + } + + async compute(key, data, hash) { + const opts = { + name: 'HMAC', + hash: {name: hashName(hash)}, + }; + const hmacKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + opts, + false, + ['sign'], + ), "importKey"); + const buffer = await subtleCryptoResult(this._subtleCrypto.sign( + opts, + hmacKey, + data, + ), "sign"); + return new Uint8Array(buffer); + } +} + +class CryptoDeriveDriver { + constructor(subtleCrypto, cryptoDriver, cryptoExtras) { + this._subtleCrypto = subtleCrypto; + this._cryptoDriver = cryptoDriver; + this._cryptoExtras = cryptoExtras; + } + /** + * [pbkdf2 description] + * @param {BufferSource} password + * @param {Number} iterations + * @param {BufferSource} salt + * @param {HashName} hash + * @param {Number} length the desired length of the generated key, in bits (not bytes!) + * @return {BufferSource} + */ + async pbkdf2(password, iterations, salt, hash, length) { + if (!this._subtleCrypto.deriveBits) { + throw new Error("PBKDF2 is not supported"); + } + const key = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + password, + {name: 'PBKDF2'}, + false, + ['deriveBits'], + ), "importKey"); + const keybits = await subtleCryptoResult(this._subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt, + iterations, + hash: hashName(hash), + }, + key, + length, + ), "deriveBits"); + return new Uint8Array(keybits); + } + + /** + * [hkdf description] + * @param {BufferSource} key [description] + * @param {BufferSource} salt [description] + * @param {BufferSource} info [description] + * @param {HashName} hash the hash to use + * @param {Number} length desired length of the generated key in bits (not bytes!) + * @return {[type]} [description] + */ + async hkdf(key, salt, info, hash, length) { + if (!this._subtleCrypto.deriveBits) { + return this._cryptoExtras.hkdf(this._cryptoDriver, key, salt, info, hash, length); + } + const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + {name: "HKDF"}, + false, + ["deriveBits"], + ), "importKey"); + const keybits = await subtleCryptoResult(this._subtleCrypto.deriveBits({ + name: "HKDF", + salt, + info, + hash: hashName(hash), + }, + hkdfkey, + length, + ), "deriveBits"); + return new Uint8Array(keybits); + } +} + +class CryptoAESDriver { + constructor(subtleCrypto) { + this._subtleCrypto = subtleCrypto; + } + /** + * [decrypt description] + * @param {BufferSource} key [description] + * @param {BufferSource} iv [description] + * @param {BufferSource} ciphertext [description] + * @return {BufferSource} [description] + */ + async decrypt(key, iv, ciphertext) { + const opts = { + name: "AES-CTR", + counter: iv, + length: 64, + }; + let aesKey; + try { + aesKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + opts, + false, + ['decrypt'], + ), "importKey"); + } catch (err) { + throw new Error(`Could not import key for AES-CTR decryption: ${err.message}`); + } + try { + const plaintext = await subtleCryptoResult(this._subtleCrypto.decrypt( + // see https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams + opts, + aesKey, + ciphertext, + ), "decrypt"); + return new Uint8Array(plaintext); + } catch (err) { + throw new Error(`Could not decrypt with AES-CTR: ${err.message}`); + } + } +} + + +class CryptoLegacyAESDriver { + constructor(aesjs) { + this._aesjs = aesjs; + } + /** + * [decrypt description] + * @param {BufferSource} key [description] + * @param {BufferSource} iv [description] + * @param {BufferSource} ciphertext [description] + * @return {BufferSource} [description] + */ + async decrypt(key, iv, ciphertext) { + const aesjs = this._aesjs; + var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); + return aesCtr.decrypt(ciphertext); + } +} + +function hashName(name) { + if (name !== "SHA-256" && name !== "SHA-512") { + throw new Error(`Invalid hash name: ${name}`); + } + return name; +} + +export class CryptoDriver { + constructor(cryptoExtras) { + const crypto = window.crypto || window.msCrypto; + const subtleCrypto = crypto.subtle || crypto.webkitSubtle; + this._subtleCrypto = subtleCrypto; + // not exactly guaranteeing AES-CTR support + // but in practice IE11 doesn't have this + if (!subtleCrypto.deriveBits && cryptoExtras.aesjs) { + this.aes = new CryptoLegacyAESDriver(cryptoExtras.aesjs); + } else { + this.aes = new CryptoAESDriver(subtleCrypto); + } + this.hmac = new CryptoHMACDriver(subtleCrypto); + this.derive = new CryptoDeriveDriver(subtleCrypto, this, cryptoExtras); + } + + /** + * [digest description] + * @param {HashName} hash + * @param {BufferSource} data + * @return {BufferSource} + */ + async digest(hash, data) { + return await subtleCryptoResult(this._subtleCrypto.digest(hashName(hash), data)); + } + + digestSize(hash) { + switch (hashName(hash)) { + case "SHA-512": return 64; + case "SHA-256": return 32; + default: throw new Error(`Not implemented for ${hashName(hash)}`); + } + } +} diff --git a/src/utils/crypto/hkdf.js b/src/utils/crypto/hkdf.js new file mode 100644 index 00000000..d46dc496 --- /dev/null +++ b/src/utils/crypto/hkdf.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2018 Jun Kurihara + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * MIT LICENSE, See https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-hkdf/LICENSE + * Based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-hkdf/src/hkdf.ts + */ + +// forked this code to make it use the cryptoDriver for HMAC that is more backwards-compatible +export async function hkdf(cryptoDriver, key, salt, info, hash, length) { + length = length / 8; + const len = cryptoDriver.digestSize(hash); + + // RFC5869 Step 1 (Extract) + const prk = await cryptoDriver.hmac.compute(salt, key, hash); + + // RFC5869 Step 2 (Expand) + let t = new Uint8Array([]); + const okm = new Uint8Array(Math.ceil(length / len) * len); + for(let i = 0; i < Math.ceil(length / len); i++){ + const concat = new Uint8Array(t.length + info.length + 1); + concat.set(t); + concat.set(info, t.length); + concat.set(new Uint8Array([i+1]), t.length + info.length); + t = await cryptoDriver.hmac.compute(prk, concat, hash); + okm.set(t, len * i); + } + return okm.slice(0, length); +} diff --git a/src/utils/crypto/pbkdf2.js b/src/utils/crypto/pbkdf2.js new file mode 100644 index 00000000..5a239791 --- /dev/null +++ b/src/utils/crypto/pbkdf2.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 Jun Kurihara + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * MIT LICENSE, See https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-pbkdf/LICENSE + * Based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-pbkdf/src/pbkdf.ts + */ + +// not used atm, but might in the future +// forked this code to make it use the cryptoDriver for HMAC that is more backwards-compatible + + +const nwbo = (num, len) => { + const arr = new Uint8Array(len); + for(let i=0; i> ((len - i - 1)*8)); + return arr; +}; + +export async function pbkdf2(cryptoDriver, password, iterations, salt, hash, length) { + const dkLen = length / 8; + if (iterations <= 0) { + throw new Error('InvalidIterationCount'); + } + if (dkLen <= 0) { + throw new Error('InvalidDerivedKeyLength'); + } + const hLen = cryptoDriver.digestSize(hash); + if(dkLen > (Math.pow(2, 32) - 1) * hLen) throw new Error('DerivedKeyTooLong'); + + const l = Math.ceil(dkLen/hLen); + const r = dkLen - (l-1)*hLen; + + const funcF = async (i) => { + const seed = new Uint8Array(salt.length + 4); + seed.set(salt); + seed.set(nwbo(i+1, 4), salt.length); + let u = await cryptoDriver.hmac.compute(password, seed, hash); + let outputF = new Uint8Array(u); + for(let j = 1; j < iterations; j++){ + if ((j % 1000) === 0) { + console.log(j, j/iterations); + } + u = await cryptoDriver.hmac.compute(password, u, hash); + outputF = u.map( (elem, idx) => elem ^ outputF[idx]); + } + return {index: i, value: outputF}; + }; + + const Tis = []; + const DK = new Uint8Array(dkLen); + for(let i = 0; i < l; i++) { + Tis.push(funcF(i)); + } + const TisResolved = await Promise.all(Tis); + TisResolved.forEach(elem => { + if (elem.index !== l - 1) { + DK.set(elem.value, elem.index*hLen); + } + else { + DK.set(elem.value.slice(0, r), elem.index*hLen); + } + }); + + return DK; +}