mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +01:00
add crypto driver with primitives needed for 4S & session backup
This commit is contained in:
parent
27ff6fc6b1
commit
00eade1c16
@ -1,2 +1,6 @@
|
|||||||
import aesjs from "../lib/aes-js/index.js";
|
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 {BrawlView} from "./ui/web/BrawlView.js";
|
||||||
import {Clock} from "./ui/web/dom/Clock.js";
|
import {Clock} from "./ui/web/dom/Clock.js";
|
||||||
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
|
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
|
||||||
|
import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js";
|
||||||
import {WorkerPool} from "./utils/WorkerPool.js";
|
import {WorkerPool} from "./utils/WorkerPool.js";
|
||||||
import {OlmWorker} from "./matrix/e2ee/OlmWorker.js";
|
import {OlmWorker} from "./matrix/e2ee/OlmWorker.js";
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ export async function main(container, paths, legacyExtras) {
|
|||||||
sessionInfoStorage,
|
sessionInfoStorage,
|
||||||
request,
|
request,
|
||||||
clock,
|
clock,
|
||||||
|
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
|
||||||
olmPromise,
|
olmPromise,
|
||||||
workerPromise,
|
workerPromise,
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,7 @@ const PICKLE_KEY = "DEFAULT_KEY";
|
|||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
// sessionInfo contains deviceId, userId and homeServer
|
// 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._clock = clock;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._hsApi = hsApi;
|
this._hsApi = hsApi;
|
||||||
|
@ -42,7 +42,7 @@ export const LoginFailure = createEnum(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export class SessionContainer {
|
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._random = random;
|
||||||
this._clock = clock;
|
this._clock = clock;
|
||||||
this._onlineStatus = onlineStatus;
|
this._onlineStatus = onlineStatus;
|
||||||
@ -60,6 +60,7 @@ export class SessionContainer {
|
|||||||
this._storage = null;
|
this._storage = null;
|
||||||
this._olmPromise = olmPromise;
|
this._olmPromise = olmPromise;
|
||||||
this._workerPromise = workerPromise;
|
this._workerPromise = workerPromise;
|
||||||
|
this._cryptoDriver = cryptoDriver;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewSessionId() {
|
createNewSessionId() {
|
||||||
@ -159,7 +160,7 @@ export class SessionContainer {
|
|||||||
}
|
}
|
||||||
this._session = new Session({storage: this._storage,
|
this._session = new Session({storage: this._storage,
|
||||||
sessionInfo: filteredSessionInfo, hsApi, olm,
|
sessionInfo: filteredSessionInfo, hsApi, olm,
|
||||||
clock: this._clock, olmWorker});
|
clock: this._clock, olmWorker, cryptoDriver: this._cryptoDriver});
|
||||||
await this._session.load();
|
await this._session.load();
|
||||||
this._status.set(LoadStatus.SessionSetup);
|
this._status.set(LoadStatus.SessionSetup);
|
||||||
await this._session.beforeFirstSync(isNewLogin);
|
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 New Issue
Block a user