forked from mystiq/hydrogen-web
add crypto driver with primitives needed for 4S & session backup
This commit is contained in:
parent
27ff6fc6b1
commit
00eade1c16
7 changed files with 362 additions and 4 deletions
|
@ -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}};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
257
src/ui/web/dom/CryptoDriver.js
Normal file
257
src/ui/web/dom/CryptoDriver.js
Normal 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
29
src/utils/crypto/hkdf.js
Normal 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);
|
||||
}
|
65
src/utils/crypto/pbkdf2.js
Normal file
65
src/utils/crypto/pbkdf2.js
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue