add crypto driver with primitives needed for 4S & session backup

This commit is contained in:
Bruno Windels 2020-09-17 10:19:09 +02:00
parent 27ff6fc6b1
commit 00eade1c16
7 changed files with 362 additions and 4 deletions

View File

@ -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}};

View File

@ -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,
});

View File

@ -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;

View File

@ -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);

View File

@ -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)}`);
}
}
}

29
src/utils/crypto/hkdf.js Normal file
View File

@ -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);
}

View File

@ -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++) arr[i] = 0xFF && (num >> ((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;
}