forked from mystiq/hydrogen-web
Merge pull request #103 from vector-im/bwindels/session-backup
Read from session backup
This commit is contained in:
commit
79ac939c8f
46 changed files with 3567 additions and 84 deletions
|
@ -47,7 +47,11 @@
|
|||
"xxhashjs": "^0.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"aes-js": "^3.1.2",
|
||||
"another-json": "^0.2.0",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz"
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"bs58": "^4.0.1",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||
"text-encoding": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
|
165
prototypes/derive-key-ie11.html
Normal file
165
prototypes/derive-key-ie11.html
Normal file
|
@ -0,0 +1,165 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://unpkg.com/text-encoding@0.6.4/lib/encoding-indexes.js"></script>
|
||||
<script src="https://unpkg.com/text-encoding@0.6.4/lib/encoding.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
|
||||
<script src="deps/jsSHA/dist/sha512.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.rawgit.com/ricmoo/aes-js/e27b99df/index.js"></script>
|
||||
<script type="text/javascript" src="derive-keys-bundle.js"></script>
|
||||
<script type="text/javascript">
|
||||
if (!Math.imul) Math.imul = function(a, b) {
|
||||
var aHi = (a >>> 16) & 0xffff;
|
||||
var aLo = a & 0xffff;
|
||||
var bHi = (b >>> 16) & 0xffff;
|
||||
var bLo = b & 0xffff;
|
||||
// the shift by 0 fixes the sign on the high part
|
||||
// the final |0 converts the unsigned value into a signed value
|
||||
return ((aLo * bLo) + (((aHi * bLo + aLo * bHi) << 16) >>> 0) | 0);
|
||||
};
|
||||
|
||||
if (!Math.clz32) Math.clz32 = (function(log, LN2){
|
||||
return function(x) {
|
||||
// Let n be ToUint32(x).
|
||||
// Let p be the number of leading zero bits in
|
||||
// the 32-bit binary representation of n.
|
||||
// Return p.
|
||||
var asUint = x >>> 0;
|
||||
if (asUint === 0) {
|
||||
return 32;
|
||||
}
|
||||
return 31 - (log(asUint) / LN2 | 0) |0; // the "| 0" acts like math.floor
|
||||
};
|
||||
})(Math.log, Math.LN2);
|
||||
</script>
|
||||
<script type="text/javascript" src="../lib/olm/olm_legacy.js"></script>
|
||||
<script type="text/javascript">
|
||||
// sample data from account with recovery key
|
||||
const ssssKeyAccountData = {
|
||||
"type": "m.secret_storage.key.le4jDjlxrIMZDSKu1EudJL5Tc4U5qI0d",
|
||||
"content": {
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
"iv": "YPhwwArIUTwasbROMFd1PQ==",
|
||||
"mac": "khWXeBzKtZi8SX6I7m/9yPoLB1yv1u9l+NNi6WF4+ek="
|
||||
}
|
||||
};
|
||||
const megolmBackupKeyAccountData = {
|
||||
"type": "m.megolm_backup.v1",
|
||||
"content": {
|
||||
"encrypted": {
|
||||
"le4jDjlxrIMZDSKu1EudJL5Tc4U5qI0d": {
|
||||
"iv": "PiqYdySj9s4RsaLc1oDF1w==",
|
||||
"ciphertext": "62fjUs1xkF3BvqVEvAEoDH9jcYiotkcJHG/VNtzSrPBlrmOYQyPA93L2rKo=",
|
||||
"mac": "vtq+kEg5XaRdw08aPiQi7+w9qUiDCQKo/jKNTvrN4ho="
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const backupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"auth_data": {
|
||||
"public_key": "tY/jSdfy2q1pS8Ux+LP8xr/RMn9NDElwofH+E5sFG38",
|
||||
"signatures": {
|
||||
"@bruno-test4s2:matrix.org": {
|
||||
"ed25519:KTLGZUJCYZ": "YPuzpLo4OZL5+HQTkbDnUKpIuCmL50Q7RnMs9cRfKqyS+CMPm0RBU1ttPO6XOZ+TjZ4VThXU50LUkmpJiKM+Aw",
|
||||
"ed25519:l17fdsfeS7qUKIYzgx3LxIcHnjPM00+Ge5dTk7Msy04": "epDo+d9foXXcnXChZaEOCKNYzofOMBXQF3FCMDJ52hxvxh9K1w+2zOOAwWEKOts88gubgIsdRQedkuhuIm2LCg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"count": 1,
|
||||
"etag": "1",
|
||||
"version": "1"
|
||||
};
|
||||
|
||||
const sessionResponse = {
|
||||
"first_message_index": 0,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": true,
|
||||
"session_data": {
|
||||
"ciphertext": "+a8OCF0v5U5GYTNAMwgNEqSItxy4hea073zlWCp+ocr4mUQDuUZyOo+DGHDPPvSOnhJA2waSV05wna/Jmig7NAzuJJy8eEd0dHmGiA16eUMFiUz0HYFseDXs0dDGF38shz1C6CXYRjTOS3S7JWLVzeeYy632BMGvGjWMvAuOpm4NgV9fLB5J6nYVb/wvU3Mf8mw/eT5k8AUJA/CAD6zM7T9skEJhuFoi5kdPfBoozUbScA5xcPVmE6aY08zZ6QpiZ7lsyWoIRDbRxaBxL82T2CnpcngE/SAHF+eJ9ZWK3txolYLT/KAfKlAVLV7yWXkYL7oxrW8DI/5ZQFXUqzqqqfAB7Qz2AIvCdUVqhDGwuDr5noCMlKYEwyYR0VC2i4ZyXdtLdOjKBS2eTqDcwdv2gcaOnbJJcIEuGMKVg89/rKqpWncY/+NOBTQhuts05+Wi+9wU+OlGlNFvhkOgp1BaP0Q7T4pkxgj4OSbf3t1UfthltJSX8TS9ZGd3DVDI8swQuMBvF9H+7kAeO2IWTMSe57MYvlk0aw/gPFdI06lcOvH2nAr9C2HNsuYhyO4XGZOAg8HHzkjLlzNU+zJk1MfRIXRoVgbIh1hApcK9HhyTBzg",
|
||||
"ephemeral": "z0JE6swJZbrmRYOWGvEI6zhIzoJ57lhzp1uujVS2jUs",
|
||||
"mac": "+AAASqA+4U8"
|
||||
}
|
||||
};
|
||||
const keyId = "le4jDjlxrIMZDSKu1EudJL5Tc4U5qI0d";
|
||||
// sample data with account with recovery passphrase
|
||||
// const ssssKeyAccountData =
|
||||
// {
|
||||
// "type": "m.secret_storage.key.HB6AKfUD4avkZfPfyjcJ6iJPWDp4f9WM",
|
||||
// "content": {
|
||||
// "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
// "passphrase": {
|
||||
// "algorithm": "m.pbkdf2",
|
||||
// "iterations": 500000,
|
||||
// "salt": "tfY5mgvQBr3Gd5Dy1IBiKf7fLquL4Y9O"
|
||||
// },
|
||||
// "iv": "xitm4hxsqagkbyEmXj0tUw==",
|
||||
// "mac": "nagOYz7FKrdlFEKM9ij78th0O2p7YVGgl+p0LHr4EBE="
|
||||
// }
|
||||
// };
|
||||
|
||||
// const megolmBackupKeyAccountData = {
|
||||
// "type": "m.megolm_backup.v1",
|
||||
// "content": {
|
||||
// "encrypted": {
|
||||
// "HB6AKfUD4avkZfPfyjcJ6iJPWDp4f9WM": {
|
||||
// "iv": "HpzOY5DxYFJCxw5Vi6BBOQ==",
|
||||
// "ciphertext": "u1TJjaaGKVDGExg9hu2fIUZ0gjToMcMReyhn4nsXgnhm7Dvz6E/4p+nSF3w=",
|
||||
// "mac": "08ckDbQK9wB2jiE4n4sfp2sw83q/0C2/gEz2LuHMEPg="
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// const backupInfo = {
|
||||
// "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
// "auth_data": {
|
||||
// "public_key": "Vw2cwhbxFg/GQ2rr4VRIQ+Oh74lP7IxY6oN4R9q992k",
|
||||
// "signatures": {
|
||||
// "@bruno-test4s:matrix.org": {
|
||||
// "ed25519:XAIKJXBCNZ": "AFBp1T2x8hyPSi2hCHg6IzNy67RxULj3/7LYZgVT3Ruz49v5h1+jAScTxZj5jrItxo2LCzSORH+yABHjPIqOBQ",
|
||||
// "ed25519:lukepZkTmPcJS6wCl12B0tIURIO8YbMd5QJLf8UOugI": "a1ZJa+1+p9Gm5Po1B619ZDy4xidHmLt82vXVPH7vWTjny1r3JI2iM4fB2qh8vEiASNlFyVrFx//gQrz9Y1IJBA"
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// "count": 1,
|
||||
// "etag": "1",
|
||||
// "version": "1"
|
||||
// };
|
||||
// const sessionResponse = {
|
||||
// "first_message_index": 0,
|
||||
// "forwarded_count": 0,
|
||||
// "is_verified": true,
|
||||
// "session_data": {
|
||||
// "ciphertext": "1NoC8/GZWeGjneuoFDcqpbMYOJ8bjDFiw2O4/YOKC59x9RqSejLyM8qLL5FzlV+uW7anPVED8t9m+p2t1kKa15LxlcdzXjLPCv1QGYlhotbUhN8eRUobQuLqsD5Dl/QqNxv+Xl65tEaQhUeF30NmSesw6GHvP93vB3mTN8Yz9QyaQtvgoI/Q6c4d+yGmFVE2dlhXdOs7Hrylrg8UyM1QI+qpNJ3L9ETcqiXCG/FJIdM87LmNnHPX65TWK5xsu1JKWCI2BY1KFVDyxm40FyHHypUPYoT9RqPnygHtYoTiZzyaVxqUu2vg08Bv0t1VH2SNDGs5aZYQN5S1JNAHrXE+cWSg0rfVb160Z4FJC/89wO8fw/uXqJehqMVuC9BSU/zsKcZ797U92qDnIb6QQuMYKRgh9JrEugqJN9ocL7F8W9fW2oFfUYRyvOZRSf387hGrapEGBKx7Owb7UoXvWyb4C5hc5SFNvej+yg98+Fi4hzlGH26DqzJdLcxU5P/MWfZc222QqPFuFspe6f0Ts5jnJhjCQhXWoM4G6mtvGbOm2ESSJULj8U4JSDz8GsxrmojR/pBpywBvuy/mx//htnacnTRqYJz+PZVtV63rfaZlEtU",
|
||||
// "ephemeral": "wXBeLoazggBmFS0eiVY9H/qq5o1yt2/NIKWcq384EHc",
|
||||
// "mac": "w3IfO5vL9Bc"
|
||||
// }
|
||||
// };
|
||||
//const keyId = "HB6AKfUD4avkZfPfyjcJ6iJPWDp4f9WM";
|
||||
const cryptoDriver = new bundle.CryptoDriver((window.crypto || window.msCrypto).subtle);
|
||||
window.Olm.init().then(function() {
|
||||
bundle.deserializeSSSSKey("EsUH dBfj L7XF Kdej TNmK 2CdP R7NQ KnQH zA1o 8kDg piuJ QEZh", ssssKeyAccountData).then(function(ssssKey) {
|
||||
//bundle.deriveSSSSKey(cryptoDriver, prompt("passphrase"), ssssKeyAccountData).then(function(ssssKey) {
|
||||
// const ssssKey = new Uint8Array(32);
|
||||
// const bytes = [123, 47, 138, 15, 190, 69, 224, 204, 88, 246, 203, 65, 243, 234, 91, 17, 250, 107, 104, 51, 211, 252, 81, 67, 80, 191, 105, 208, 127, 87, 107, 231];
|
||||
// for (var i = bytes.length - 1; i >= 0; i--) {
|
||||
// ssssKey[i] = bytes[i];
|
||||
// }
|
||||
console.log("ssssKey", ssssKey);
|
||||
bundle.decryptSecret(cryptoDriver, keyId, ssssKey, megolmBackupKeyAccountData).then(function(backupKeyBase64) {
|
||||
console.log("backupKeyBase64", backupKeyBase64);
|
||||
bundle.decryptSession(backupKeyBase64, backupInfo, sessionResponse).then(function(session) {
|
||||
console.log("session", session);
|
||||
alert(session.session_key);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
458
prototypes/derive-keys.js
vendored
Normal file
458
prototypes/derive-keys.js
vendored
Normal file
|
@ -0,0 +1,458 @@
|
|||
import {base58} from "../src/utils/base-encoding.js";
|
||||
|
||||
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 = e => 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class CryptoLegacyHMACDriver {
|
||||
constructor(hmacDriver) {
|
||||
this._hmacDriver = hmacDriver;
|
||||
}
|
||||
|
||||
async verify(key, mac, data, hash) {
|
||||
if (hash === "SHA-512") {
|
||||
throw new Error("SHA-512 HMAC verification is not implemented yet");
|
||||
} else {
|
||||
return this._hmacDriver.verify(key, mac, data, hash)
|
||||
}
|
||||
}
|
||||
|
||||
async compute(key, data, hash) {
|
||||
if (hash === "SHA-256") {
|
||||
return await this._hmacDriver.compute(key, data, hash);
|
||||
} else {
|
||||
const shaObj = new window.jsSHA(hash, "UINT8ARRAY", {
|
||||
"hmacKey": {
|
||||
"value": key,
|
||||
"format": "UINT8ARRAY"
|
||||
}
|
||||
});
|
||||
shaObj.update(data);
|
||||
return shaObj.getHash("UINT8ARRAY");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoLegacyDeriveDriver {
|
||||
constructor(cryptoDriver) {
|
||||
this._cryptoDriver = cryptoDriver;
|
||||
}
|
||||
|
||||
// adapted from https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-pbkdf/src/pbkdf.ts#L21
|
||||
// could also consider https://github.com/brix/crypto-js/blob/develop/src/pbkdf2.js although not async
|
||||
async pbkdf2(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 = this._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 this._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 this._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;
|
||||
}
|
||||
|
||||
// based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-hkdf/src/hkdf.ts
|
||||
async hkdf(key, salt, info, hash, length) {
|
||||
length = length / 8;
|
||||
const len = this._cryptoDriver.digestSize(hash);
|
||||
|
||||
// RFC5869 Step 1 (Extract)
|
||||
const prk = await this._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 this._cryptoDriver.hmac.compute(prk, concat, hash);
|
||||
okm.set(t, len * i);
|
||||
}
|
||||
return okm.slice(0, length);
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoDeriveDriver {
|
||||
constructor(subtleCrypto) {
|
||||
this._subtleCrypto = subtleCrypto;
|
||||
}
|
||||
/**
|
||||
* [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) {
|
||||
// check for existance of deriveBits, which IE11 does not have
|
||||
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) {
|
||||
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 {
|
||||
/**
|
||||
* [decrypt description]
|
||||
* @param {BufferSource} key [description]
|
||||
* @param {BufferSource} iv [description]
|
||||
* @param {BufferSource} ciphertext [description]
|
||||
* @return {BufferSource} [description]
|
||||
*/
|
||||
async decrypt(key, iv, ciphertext) {
|
||||
const aesjs = window.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(subtleCrypto) {
|
||||
this.aes = new CryptoLegacyAESDriver();
|
||||
// this.aes = new CryptoAESDriver(subtleCrypto);
|
||||
//this.derive = new CryptoDeriveDriver(subtleCrypto);
|
||||
this.derive = new CryptoLegacyDeriveDriver(this);
|
||||
// subtleCrypto.deriveBits ?
|
||||
// new CryptoDeriveDriver(subtleCrypto) :
|
||||
// new CryptoLegacyDeriveDriver(this);
|
||||
this.hmac = new CryptoLegacyHMACDriver(new CryptoHMACDriver(subtleCrypto));
|
||||
this._subtleCrypto = subtleCrypto;
|
||||
}
|
||||
|
||||
/**
|
||||
* [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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeBase64(base64) {
|
||||
const binStr = window.atob(base64);
|
||||
const len = binStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
const DEFAULT_BITSIZE = 256;
|
||||
|
||||
export async function deriveSSSSKey(cryptoDriver, passphrase, ssssKey) {
|
||||
const textEncoder = new TextEncoder();
|
||||
return await cryptoDriver.derive.pbkdf2(
|
||||
textEncoder.encode(passphrase),
|
||||
ssssKey.content.passphrase.iterations || DEFAULT_ITERATIONS,
|
||||
textEncoder.encode(ssssKey.content.passphrase.salt),
|
||||
"SHA-512",
|
||||
ssssKey.content.passphrase.bits || DEFAULT_BITSIZE);
|
||||
}
|
||||
|
||||
export async function decryptSecret(cryptoDriver, keyId, ssssKey, event) {
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
// now derive the aes and mac key from the 4s key
|
||||
const hkdfKey = await cryptoDriver.derive.hkdf(
|
||||
ssssKey,
|
||||
new Uint8Array(8).buffer, //salt
|
||||
textEncoder.encode(event.type), // info
|
||||
"SHA-256",
|
||||
512 // 512 bits or 64 bytes
|
||||
);
|
||||
const aesKey = hkdfKey.slice(0, 32);
|
||||
const hmacKey = hkdfKey.slice(32);
|
||||
|
||||
const data = event.content.encrypted[keyId];
|
||||
|
||||
const ciphertextBytes = decodeBase64(data.ciphertext);
|
||||
const isVerified = await cryptoDriver.hmac.verify(
|
||||
hmacKey, decodeBase64(data.mac),
|
||||
ciphertextBytes, "SHA-256");
|
||||
|
||||
if (!isVerified) {
|
||||
throw new Error("Bad MAC");
|
||||
}
|
||||
|
||||
const plaintext = await cryptoDriver.aes.decrypt(aesKey, decodeBase64(data.iv), ciphertextBytes);
|
||||
return textDecoder.decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
|
||||
export async function decryptSession(backupKeyBase64, backupInfo, sessionResponse) {
|
||||
const privKey = decodeBase64(backupKeyBase64);
|
||||
console.log("privKey", privKey);
|
||||
|
||||
const decryption = new window.Olm.PkDecryption();
|
||||
let backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has enetered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (backupPubKey !== backupInfo.auth_data.public_key) {
|
||||
console.log("backupPubKey", backupPubKey.length, backupPubKey);
|
||||
throw new Error("bad backup key");
|
||||
}
|
||||
|
||||
const sessionInfo = decryption.decrypt(
|
||||
sessionResponse.session_data.ephemeral,
|
||||
sessionResponse.session_data.mac,
|
||||
sessionResponse.session_data.ciphertext,
|
||||
);
|
||||
return JSON.parse(sessionInfo);
|
||||
}
|
||||
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
|
||||
export async function deserializeSSSSKey(recoverykey) {
|
||||
const result = base58.decode(recoverykey.replace(/ /g, ''));
|
||||
|
||||
let parity = 0;
|
||||
for (const b of result) {
|
||||
parity ^= b;
|
||||
}
|
||||
if (parity !== 0) {
|
||||
throw new Error("Incorrect parity");
|
||||
}
|
||||
|
||||
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
|
||||
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||
throw new Error("Incorrect prefix");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.length !==
|
||||
OLM_RECOVERY_KEY_PREFIX.length + window.Olm.PRIVATE_KEY_LENGTH + 1
|
||||
) {
|
||||
throw new Error("Incorrect length");
|
||||
}
|
||||
|
||||
return Uint8Array.from(result.slice(
|
||||
OLM_RECOVERY_KEY_PREFIX.length,
|
||||
OLM_RECOVERY_KEY_PREFIX.length + window.Olm.PRIVATE_KEY_LENGTH,
|
||||
));
|
||||
}
|
91
prototypes/ie11-hmac.html
Normal file
91
prototypes/ie11-hmac.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://dl.dropboxusercontent.com/s/r55397ld512etib/EncoderDecoderTogether.min.js?dl=0" nomodule="" type="text/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
|
||||
<script src="deps/jsSHA/dist/sha512.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function decodeBase64(base64) {
|
||||
const binStr = window.atob(base64);
|
||||
const len = binStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function encodeBase64(bytes) {
|
||||
let binStr = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binStr += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binStr);
|
||||
}
|
||||
|
||||
function subtleCryptoResult(promiseOrOp, method) {
|
||||
if (promiseOrOp instanceof Promise) {
|
||||
return promiseOrOp;
|
||||
} else {
|
||||
return new Promise(function(resolve, reject) {
|
||||
promiseOrOp.oncomplete = function(e) {resolve(e.target.result);}
|
||||
promiseOrOp.onerror = function(e) {
|
||||
reject(new Error("Crypto error on " + method));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const subtleCrypto = (window.crypto || window.msCrypto).subtle;
|
||||
|
||||
function computeFallback(key, data, hash) {
|
||||
const shaObj = new jsSHA(hash, "UINT8ARRAY", {
|
||||
"hmacKey": {
|
||||
"value": key,
|
||||
"format": "UINT8ARRAY"
|
||||
}
|
||||
});
|
||||
shaObj.update(data);
|
||||
return Promise.resolve(shaObj.getHash("UINT8ARRAY"));
|
||||
}
|
||||
|
||||
function compute(key, data, hash) {
|
||||
const opts = {
|
||||
name: 'HMAC',
|
||||
hash: {name: hash},
|
||||
};
|
||||
return subtleCryptoResult(subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
opts,
|
||||
false,
|
||||
['sign']
|
||||
), "importKey").then(function (hmacKey) {
|
||||
console.log("hmacKey", hmacKey);
|
||||
return subtleCryptoResult(subtleCrypto.sign(
|
||||
opts,
|
||||
hmacKey,
|
||||
data
|
||||
), "sign");
|
||||
}).then(function(buffer) {
|
||||
return new Uint8Array(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
const te = new TextEncoder();
|
||||
computeFallback(
|
||||
new Uint8Array(te.encode("I am a key!!")),
|
||||
new Uint8Array(te.encode("I am some data!!")),
|
||||
"SHA-512"
|
||||
).then(function(mac) {
|
||||
// should be 9bpJS7myNR/ttCfts+woXJSapVb19qqFRntGh17rHydOBB8+pplZFG8Cc4Qkxxznri4nWyzhFWcWnenY9vd5rA==
|
||||
alert(encodeBase64(mac));
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
62
prototypes/pkcrypto-ie11.html
Normal file
62
prototypes/pkcrypto-ie11.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
if (!Math.imul) Math.imul = function(a, b) {
|
||||
var aHi = (a >>> 16) & 0xffff;
|
||||
var aLo = a & 0xffff;
|
||||
var bHi = (b >>> 16) & 0xffff;
|
||||
var bLo = b & 0xffff;
|
||||
// the shift by 0 fixes the sign on the high part
|
||||
// the final |0 converts the unsigned value into a signed value
|
||||
return ((aLo * bLo) + (((aHi * bLo + aLo * bHi) << 16) >>> 0) | 0);
|
||||
};
|
||||
|
||||
if (!Math.clz32) Math.clz32 = (function(log, LN2){
|
||||
return function(x) {
|
||||
// Let n be ToUint32(x).
|
||||
// Let p be the number of leading zero bits in
|
||||
// the 32-bit binary representation of n.
|
||||
// Return p.
|
||||
var asUint = x >>> 0;
|
||||
if (asUint === 0) {
|
||||
return 32;
|
||||
}
|
||||
return 31 - (log(asUint) / LN2 | 0) |0; // the "| 0" acts like math.floor
|
||||
};
|
||||
})(Math.log, Math.LN2);
|
||||
</script>
|
||||
<!-- removing this line will make it work -->
|
||||
<script src="https://dl.dropboxusercontent.com/s/r55397ld512etib/EncoderDecoderTogether.min.js?dl=0" nomodule="" type="text/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
|
||||
<script type="text/javascript" src="../lib/olm/olm_legacy.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.Olm.init().then(function() {
|
||||
const bytes = [
|
||||
34, 123, 54, 9, 124, 89, 230, 120,
|
||||
43, 232, 19, 78, 129, 170, 255, 5,
|
||||
90, 143, 56, 99, 101, 140, 240, 3,
|
||||
7, 121, 41, 22, 67, 231, 85, 32
|
||||
];
|
||||
var privKey = new Uint8Array(32);
|
||||
for (var i = bytes.length - 1; i >= 0; i--) {
|
||||
privKey[i] = bytes[i];
|
||||
}
|
||||
console.log("privKey", privKey);
|
||||
const decryption = new window.Olm.PkDecryption();
|
||||
let backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
console.log("backupPubKey", backupPubKey.length, backupPubKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
23
prototypes/tools/package.json
Normal file
23
prototypes/tools/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "foo",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@rollup/plugin-babel": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"mdn-polyfills": "^5.20.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rollup": "^2.26.4",
|
||||
"core-js": "^3.6.5"
|
||||
}
|
||||
}
|
46
prototypes/tools/transpile-ie11.mjs
Normal file
46
prototypes/tools/transpile-ie11.mjs
Normal file
|
@ -0,0 +1,46 @@
|
|||
import fsRoot from "fs";
|
||||
const fs = fsRoot.promises;
|
||||
import { rollup } from 'rollup';
|
||||
// needed for legacy bundle
|
||||
import babel from '@rollup/plugin-babel';
|
||||
// needed to find the polyfill modules in the main-legacy.js bundle
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
// needed because some of the polyfills are written as commonjs modules
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
// multi-entry plugin so we can add polyfill file to main
|
||||
import multi from '@rollup/plugin-multi-entry';
|
||||
import removeJsComments from 'rollup-plugin-cleanup';
|
||||
// replace urls of asset names with content hashed version
|
||||
|
||||
async function build(inputFile, outputFile) {
|
||||
// compile down to whatever IE 11 needs
|
||||
const babelPlugin = babel.babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: '../../node_modules/**',
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: "3",
|
||||
targets: "IE 11"
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
const polyfillFile = '../../src/worker-polyfill.js';
|
||||
// create js bundle
|
||||
const rollupConfig = {
|
||||
input: [polyfillFile, inputFile],
|
||||
plugins: [multi(), commonjs(), nodeResolve(), babelPlugin, removeJsComments({comments: "none"})]
|
||||
};
|
||||
const bundle = await rollup(rollupConfig);
|
||||
const {output} = await bundle.generate({
|
||||
format: 'iife',
|
||||
name: `bundle`
|
||||
});
|
||||
const code = output[0].code;
|
||||
await fs.writeFile(outputFile, code, "utf8");
|
||||
}
|
||||
|
||||
build(process.argv[2], process.argv[3]);
|
1485
prototypes/tools/yarn.lock
Normal file
1485
prototypes/tools/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -88,7 +88,7 @@ async function build() {
|
|||
// so do it first
|
||||
const themeAssets = await copyThemeAssets(themes, legacy);
|
||||
const jsBundlePath = await buildJs("src/main.js", `${PROJECT_ID}.js`);
|
||||
const jsLegacyBundlePath = await buildJsLegacy("src/main.js", `${PROJECT_ID}-legacy.js`);
|
||||
const jsLegacyBundlePath = await buildJsLegacy("src/main.js", `${PROJECT_ID}-legacy.js`, 'src/legacy-extras.js');
|
||||
const jsWorkerPath = await buildWorkerJsLegacy("src/worker.js", `worker.js`);
|
||||
const cssBundlePaths = await buildCssBundles(legacy ? buildCssLegacy : buildCss, themes, themeAssets);
|
||||
|
||||
|
@ -185,7 +185,7 @@ async function buildHtml(doc, version, assetPaths, manifestPath) {
|
|||
doc("script#main").replaceWith(
|
||||
`<script type="module">import {main} from "./${assetPaths.jsBundle()}"; main(document.body, ${pathsJSON});</script>` +
|
||||
`<script type="text/javascript" nomodule src="${assetPaths.jsLegacyBundle()}"></script>` +
|
||||
`<script type="text/javascript" nomodule>${PROJECT_ID}Bundle.main(document.body, ${pathsJSON});</script>`);
|
||||
`<script type="text/javascript" nomodule>${PROJECT_ID}Bundle.main(document.body, ${pathsJSON}, ${PROJECT_ID}Bundle.legacyExtras);</script>`);
|
||||
removeOrEnableScript(doc("script#service-worker"), offline);
|
||||
|
||||
const versionScript = doc("script#version");
|
||||
|
@ -218,7 +218,7 @@ async function buildJs(inputFile, outputName) {
|
|||
return bundlePath;
|
||||
}
|
||||
|
||||
async function buildJsLegacy(inputFile, outputName, polyfillFile = null) {
|
||||
async function buildJsLegacy(inputFile, outputName, extraFile, polyfillFile) {
|
||||
// compile down to whatever IE 11 needs
|
||||
const babelPlugin = babel.babel({
|
||||
babelHelpers: 'bundled',
|
||||
|
@ -237,10 +237,14 @@ async function buildJsLegacy(inputFile, outputName, polyfillFile = null) {
|
|||
if (!polyfillFile) {
|
||||
polyfillFile = 'src/legacy-polyfill.js';
|
||||
}
|
||||
const inputFiles = [polyfillFile, inputFile];
|
||||
if (extraFile) {
|
||||
inputFiles.push(extraFile);
|
||||
}
|
||||
// create js bundle
|
||||
const rollupConfig = {
|
||||
input: [polyfillFile, inputFile],
|
||||
plugins: [multi(), commonjs(), nodeResolve(), babelPlugin, removeJsComments({comments: "none"})]
|
||||
input: inputFiles,
|
||||
plugins: [multi(), commonjs(), nodeResolve(), babelPlugin]
|
||||
};
|
||||
const bundle = await rollup(rollupConfig);
|
||||
const {output} = await bundle.generate({
|
||||
|
@ -255,7 +259,7 @@ async function buildJsLegacy(inputFile, outputName, polyfillFile = null) {
|
|||
|
||||
function buildWorkerJsLegacy(inputFile, outputName) {
|
||||
const polyfillFile = 'src/worker-polyfill.js';
|
||||
return buildJsLegacy(inputFile, outputName, polyfillFile);
|
||||
return buildJsLegacy(inputFile, outputName, null, polyfillFile);
|
||||
}
|
||||
|
||||
async function buildOffline(version, assetPaths) {
|
||||
|
|
6
scripts/package-overrides/buffer/index.js
Normal file
6
scripts/package-overrides/buffer/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = class Buffer {
|
||||
static isBuffer(array) {return array instanceof Uint8Array;}
|
||||
static from(arrayBuffer) {return arrayBuffer;}
|
||||
static allocUnsafe(size) {return Buffer.alloc(size);}
|
||||
static alloc(size) {return new Uint8Array(size);}
|
||||
};
|
1
scripts/package-overrides/safe-buffer/index.js
Normal file
1
scripts/package-overrides/safe-buffer/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports.Buffer = require("buffer");
|
|
@ -22,18 +22,34 @@ import { fileURLToPath } from 'url';
|
|||
import { dirname } from 'path';
|
||||
// needed to translate commonjs modules to esm
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
// multi-entry plugin so we can add polyfill file to main
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import {removeDirIfExists} from "./common.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const projectDir = path.join(__dirname, "../");
|
||||
|
||||
/** function used to resolve common-js require calls below. */
|
||||
function packageIterator(request, start, defaultIterator) {
|
||||
// this is just working for bs58, would need to tune it further for other dependencies
|
||||
if (request === "safe-buffer") {
|
||||
return [path.join(projectDir, "/scripts/package-overrides/safe-buffer")];
|
||||
} else if (request === "buffer/") {
|
||||
return [path.join(projectDir, "/scripts/package-overrides/buffer")];
|
||||
} else {
|
||||
return defaultIterator();
|
||||
}
|
||||
}
|
||||
|
||||
async function commonjsToESM(src, dst) {
|
||||
// create js bundle
|
||||
const bundle = await rollup({
|
||||
input: src,
|
||||
plugins: [commonjs()]
|
||||
plugins: [commonjs(), nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
customResolveOptions: {packageIterator}
|
||||
})]
|
||||
});
|
||||
const {output} = await bundle.generate({
|
||||
format: 'es'
|
||||
|
@ -59,6 +75,27 @@ async function populateLib() {
|
|||
path.join(modulesDir, 'another-json/another-json.js'),
|
||||
path.join(libDir, "another-json/index.js")
|
||||
);
|
||||
// transpile bs58 to esm
|
||||
await fs.mkdir(path.join(libDir, "bs58/"));
|
||||
await commonjsToESM(
|
||||
path.join(modulesDir, 'bs58/index.js'),
|
||||
path.join(libDir, "bs58/index.js")
|
||||
);
|
||||
// transpile base64-arraybuffer to esm
|
||||
await fs.mkdir(path.join(libDir, "base64-arraybuffer/"));
|
||||
await commonjsToESM(
|
||||
path.join(modulesDir, 'base64-arraybuffer/lib/base64-arraybuffer.js'),
|
||||
path.join(libDir, "base64-arraybuffer/index.js")
|
||||
);
|
||||
// this probably should no go in here, we can just import "aes-js" from legacy-extras.js
|
||||
// as that file is never loaded from a browser
|
||||
|
||||
// transpile aesjs to esm
|
||||
await fs.mkdir(path.join(libDir, "aes-js/"));
|
||||
await commonjsToESM(
|
||||
path.join(modulesDir, 'aes-js/index.js'),
|
||||
path.join(libDir, "aes-js/index.js")
|
||||
);
|
||||
}
|
||||
|
||||
populateLib();
|
||||
|
|
|
@ -80,7 +80,7 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
async cancel() {
|
||||
try {
|
||||
if (this._sessionContainer) {
|
||||
this._sessionContainer.stop();
|
||||
this._sessionContainer.dispose();
|
||||
if (this._deleteSessionOnCancel) {
|
||||
await this._sessionContainer.deleteSession();
|
||||
}
|
||||
|
|
|
@ -31,21 +31,24 @@ const SessionStatus = createEnum(
|
|||
export class SessionStatusViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {sync, reconnector} = options;
|
||||
const {sync, reconnector, session} = options;
|
||||
this._sync = sync;
|
||||
this._reconnector = reconnector;
|
||||
this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get());
|
||||
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
start() {
|
||||
const update = () => this._updateStatus();
|
||||
this.track(this._sync.status.subscribe(update));
|
||||
this.track(this._reconnector.connectionStatus.subscribe(update));
|
||||
this.track(this._session.needsSessionBackup.subscribe(() => {
|
||||
this.emitChange();
|
||||
}));
|
||||
}
|
||||
|
||||
get isShown() {
|
||||
return this._status !== SessionStatus.Syncing;
|
||||
return this._session.needsSessionBackup.get() || this._status !== SessionStatus.Syncing;
|
||||
}
|
||||
|
||||
get statusLabel() {
|
||||
|
@ -61,6 +64,9 @@ export class SessionStatusViewModel extends ViewModel {
|
|||
case SessionStatus.SyncError:
|
||||
return this.i18n`Sync failed because of ${this._sync.error}`;
|
||||
}
|
||||
if (this._session.needsSessionBackup.get()) {
|
||||
return this.i18n`Set up secret storage to decrypt older messages.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -122,9 +128,25 @@ export class SessionStatusViewModel extends ViewModel {
|
|||
return this._status === SessionStatus.Disconnected;
|
||||
}
|
||||
|
||||
get isSecretStorageShown() {
|
||||
// TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other.
|
||||
return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get();
|
||||
}
|
||||
|
||||
connectNow() {
|
||||
if (this.isConnectNowShown) {
|
||||
this._reconnector.tryNow();
|
||||
}
|
||||
}
|
||||
|
||||
async enterPassphrase(passphrase) {
|
||||
if (passphrase) {
|
||||
try {
|
||||
await this._session.enableSecretStorage("recoverykey", passphrase);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(`Could not set up secret storage: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ export class SessionViewModel extends ViewModel {
|
|||
this._session = sessionContainer.session;
|
||||
this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({
|
||||
sync: sessionContainer.sync,
|
||||
reconnector: sessionContainer.reconnector
|
||||
reconnector: sessionContainer.reconnector,
|
||||
session: sessionContainer.session,
|
||||
})));
|
||||
this._currentRoomTileViewModel = null;
|
||||
this._currentRoomViewModel = null;
|
||||
|
|
|
@ -145,7 +145,12 @@ export class TilesCollection extends BaseObservableList {
|
|||
if (tile) {
|
||||
const action = tile.updateEntry(entry, params);
|
||||
if (action.shouldReplace) {
|
||||
this._replaceTile(tileIdx, tile, this._tileCreator(entry));
|
||||
const newTile = this._tileCreator(entry);
|
||||
if (newTile) {
|
||||
this._replaceTile(tileIdx, tile, newTile);
|
||||
} else {
|
||||
this._removeTile(tileIdx, tile);
|
||||
}
|
||||
}
|
||||
if (action.shouldRemove) {
|
||||
this._removeTile(tileIdx, tile);
|
||||
|
|
6
src/legacy-extras.js
Normal file
6
src/legacy-extras.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import aesjs from "../lib/aes-js/index.js";
|
||||
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}};
|
|
@ -18,6 +18,13 @@ limitations under the License.
|
|||
import "core-js/stable";
|
||||
import "regenerator-runtime/runtime";
|
||||
import "mdn-polyfills/Element.prototype.closest";
|
||||
// olm.init needs utf-16le, and this polyfill was
|
||||
// the only one I could find supporting it.
|
||||
// TODO: because the library sees a commonjs environment,
|
||||
// it will also include the file supporting *all* the encodings,
|
||||
// weighing a good extra 500kb :-(
|
||||
import "text-encoding";
|
||||
|
||||
// TODO: contribute this to mdn-polyfills
|
||||
if (!Element.prototype.remove) {
|
||||
Element.prototype.remove = function remove() {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
@ -78,8 +79,9 @@ async function loadOlmWorker(paths) {
|
|||
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||
// which does not support default exports,
|
||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||
export async function main(container, paths) {
|
||||
export async function main(container, paths, legacyExtras) {
|
||||
try {
|
||||
// TODO: add .legacy to body in (legacy)platform.createAndMountRootView; and use body:not(.legacy) if needed for modern stuff
|
||||
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||
if (isIE11) {
|
||||
document.body.className += " ie11";
|
||||
|
@ -122,6 +124,7 @@ export async function main(container, paths) {
|
|||
sessionInfoStorage,
|
||||
request,
|
||||
clock,
|
||||
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
|
||||
olmPromise,
|
||||
workerPromise,
|
||||
});
|
||||
|
@ -132,6 +135,7 @@ export async function main(container, paths) {
|
|||
});
|
||||
window.__brawlViewModel = vm;
|
||||
await vm.load();
|
||||
// TODO: replace with platform.createAndMountRootView(vm, container);
|
||||
const view = new BrawlView(vm);
|
||||
container.appendChild(view.mount());
|
||||
} catch(err) {
|
||||
|
|
|
@ -23,18 +23,26 @@ import {Account as E2EEAccount} from "./e2ee/Account.js";
|
|||
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js";
|
||||
import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js";
|
||||
import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js";
|
||||
import {SessionBackup} from "./e2ee/megolm/SessionBackup.js";
|
||||
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
|
||||
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
|
||||
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
|
||||
import {DeviceTracker} from "./e2ee/DeviceTracker.js";
|
||||
import {LockMap} from "../utils/LockMap.js";
|
||||
import {groupBy} from "../utils/groupBy.js";
|
||||
import {
|
||||
keyFromCredential as ssssKeyFromCredential,
|
||||
readKey as ssssReadKey,
|
||||
writeKey as ssssWriteKey,
|
||||
} from "./ssss/index.js";
|
||||
import {SecretStorage} from "./ssss/SecretStorage.js";
|
||||
import {ObservableValue} from "../observable/ObservableValue.js";
|
||||
|
||||
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;
|
||||
|
@ -54,6 +62,7 @@ export class Session {
|
|||
this._megolmDecryption = null;
|
||||
this._getSyncToken = () => this.syncToken;
|
||||
this._olmWorker = olmWorker;
|
||||
this._cryptoDriver = cryptoDriver;
|
||||
|
||||
if (olm) {
|
||||
this._olmUtil = new olm.Utility();
|
||||
|
@ -66,6 +75,7 @@ export class Session {
|
|||
});
|
||||
}
|
||||
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
||||
this.needsSessionBackup = new ObservableValue(false);
|
||||
}
|
||||
|
||||
// called once this._e2eeAccount is assigned
|
||||
|
@ -130,10 +140,62 @@ export class Session {
|
|||
megolmEncryption: this._megolmEncryption,
|
||||
megolmDecryption: this._megolmDecryption,
|
||||
storage: this._storage,
|
||||
encryptionParams
|
||||
sessionBackup: this._sessionBackup,
|
||||
encryptionParams,
|
||||
notifyMissingMegolmSession: () => {
|
||||
if (!this._sessionBackup) {
|
||||
this.needsSessionBackup.set(true)
|
||||
}
|
||||
},
|
||||
clock: this._clock
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable secret storage by providing the secret storage credential.
|
||||
* This will also see if there is a megolm session backup and try to enable that if so.
|
||||
*
|
||||
* @param {string} type either "passphrase" or "recoverykey"
|
||||
* @param {string} credential either the passphrase or the recovery key, depending on the type
|
||||
* @return {Promise} resolves or rejects after having tried to enable secret storage
|
||||
*/
|
||||
async enableSecretStorage(type, credential) {
|
||||
if (!this._olm) {
|
||||
throw new Error("olm required");
|
||||
}
|
||||
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm);
|
||||
// and create session backup, which needs to read from accountData
|
||||
const readTxn = await this._storage.readTxn([
|
||||
this._storage.storeNames.accountData,
|
||||
]);
|
||||
await this._createSessionBackup(key, readTxn);
|
||||
// only after having read a secret, write the key
|
||||
// as we only find out if it was good if the MAC verification succeeds
|
||||
const writeTxn = await this._storage.readWriteTxn([
|
||||
this._storage.storeNames.session,
|
||||
]);
|
||||
try {
|
||||
ssssWriteKey(key, writeTxn);
|
||||
} catch (err) {
|
||||
writeTxn.abort();
|
||||
throw err;
|
||||
}
|
||||
await writeTxn.complete();
|
||||
}
|
||||
|
||||
async _createSessionBackup(ssssKey, txn) {
|
||||
const secretStorage = new SecretStorage({key: ssssKey, cryptoDriver: this._cryptoDriver});
|
||||
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
|
||||
if (this._sessionBackup) {
|
||||
for (const room of this._rooms.values()) {
|
||||
if (room.isEncrypted) {
|
||||
room.enableSessionBackup(this._sessionBackup);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.needsSessionBackup.set(false);
|
||||
}
|
||||
|
||||
// called after load
|
||||
async beforeFirstSync(isNewLogin) {
|
||||
if (this._olm) {
|
||||
|
@ -155,6 +217,17 @@ export class Session {
|
|||
await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
|
||||
await this._e2eeAccount.uploadKeys(this._storage);
|
||||
await this._deviceMessageHandler.decryptPending(this.rooms);
|
||||
|
||||
const txn = await this._storage.readTxn([
|
||||
this._storage.storeNames.session,
|
||||
this._storage.storeNames.accountData,
|
||||
]);
|
||||
// try set up session backup if we stored the ssss key
|
||||
const ssssKey = await ssssReadKey(txn);
|
||||
if (ssssKey) {
|
||||
// txn will end here as this does a network request
|
||||
await this._createSessionBackup(ssssKey, txn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,9 +270,13 @@ export class Session {
|
|||
return this._sendScheduler.isStarted;
|
||||
}
|
||||
|
||||
stop() {
|
||||
dispose() {
|
||||
this._olmWorker?.dispose();
|
||||
this._sendScheduler.stop();
|
||||
this._sessionBackup?.dispose();
|
||||
for (const room of this._rooms.values()) {
|
||||
room.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async start(lastVersionResponse) {
|
||||
|
@ -289,6 +366,16 @@ export class Session {
|
|||
if (Array.isArray(toDeviceEvents)) {
|
||||
this._deviceMessageHandler.writeSync(toDeviceEvents, txn);
|
||||
}
|
||||
|
||||
// store account data
|
||||
const accountData = syncResponse["account_data"];
|
||||
if (Array.isArray(accountData?.events)) {
|
||||
for (const event of accountData.events) {
|
||||
if (typeof event.type === "string") {
|
||||
txn.accountData.set(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
@ -306,8 +393,6 @@ export class Session {
|
|||
const needsToUploadOTKs = await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
|
||||
const promises = [this._deviceMessageHandler.decryptPending(this.rooms)];
|
||||
if (needsToUploadOTKs) {
|
||||
// TODO: we could do this in parallel with sync if it proves to be too slow
|
||||
// but I'm not sure how to not swallow errors in that case
|
||||
promises.push(this._e2eeAccount.uploadKeys(this._storage));
|
||||
}
|
||||
// run key upload and decryption in parallel
|
||||
|
|
|
@ -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);
|
||||
|
@ -245,7 +246,7 @@ export class SessionContainer {
|
|||
return this._reconnector;
|
||||
}
|
||||
|
||||
stop() {
|
||||
dispose() {
|
||||
if (this._reconnectSubscription) {
|
||||
this._reconnectSubscription();
|
||||
this._reconnectSubscription = null;
|
||||
|
@ -254,7 +255,7 @@ export class SessionContainer {
|
|||
this._sync.stop();
|
||||
}
|
||||
if (this._session) {
|
||||
this._session.stop();
|
||||
this._session.dispose();
|
||||
}
|
||||
if (this._waitForFirstSyncHandle) {
|
||||
this._waitForFirstSyncHandle.dispose();
|
||||
|
|
|
@ -105,6 +105,8 @@ export class Sync {
|
|||
this._status.set(SyncStatus.Syncing);
|
||||
} catch (err) {
|
||||
if (!(err instanceof AbortError)) {
|
||||
console.warn("stopping sync because of error");
|
||||
console.error(err);
|
||||
this._error = err;
|
||||
this._status.set(SyncStatus.Stopped);
|
||||
}
|
||||
|
@ -168,8 +170,6 @@ export class Sync {
|
|||
}));
|
||||
sessionChanges = await this._session.writeSync(response, syncFilterId, syncTxn);
|
||||
} catch(err) {
|
||||
console.warn("aborting syncTxn because of error");
|
||||
console.error(err);
|
||||
// avoid corrupting state by only
|
||||
// storing the sync up till the point
|
||||
// the exception occurred
|
||||
|
@ -228,7 +228,8 @@ export class Sync {
|
|||
// to discard outbound session when somebody leaves a room
|
||||
// and to create room key messages when somebody leaves
|
||||
storeNames.outboundGroupSessions,
|
||||
storeNames.operations
|
||||
storeNames.operations,
|
||||
storeNames.accountData,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,17 @@ import {makeTxnId} from "../common.js";
|
|||
|
||||
const ENCRYPTED_TYPE = "m.room.encrypted";
|
||||
|
||||
function encodeMissingSessionKey(senderKey, sessionId) {
|
||||
return `${senderKey}|${sessionId}`;
|
||||
}
|
||||
|
||||
function decodeMissingSessionKey(key) {
|
||||
const [senderKey, sessionId] = key.split("|");
|
||||
return {senderKey, sessionId};
|
||||
}
|
||||
|
||||
export class RoomEncryption {
|
||||
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage}) {
|
||||
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) {
|
||||
this._room = room;
|
||||
this._deviceTracker = deviceTracker;
|
||||
this._olmEncryption = olmEncryption;
|
||||
|
@ -37,6 +46,21 @@ export class RoomEncryption {
|
|||
this._eventIdsByMissingSession = new Map();
|
||||
this._senderDeviceCache = new Map();
|
||||
this._storage = storage;
|
||||
this._sessionBackup = sessionBackup;
|
||||
this._notifyMissingMegolmSession = notifyMissingMegolmSession;
|
||||
this._clock = clock;
|
||||
this._disposed = false;
|
||||
}
|
||||
|
||||
async enableSessionBackup(sessionBackup) {
|
||||
if (this._sessionBackup) {
|
||||
return;
|
||||
}
|
||||
this._sessionBackup = sessionBackup;
|
||||
for(const key of this._eventIdsByMissingSession.keys()) {
|
||||
const {senderKey, sessionId} = decodeMissingSessionKey(key);
|
||||
await this._requestMissingSessionFromBackup(senderKey, sessionId, null);
|
||||
}
|
||||
}
|
||||
|
||||
notifyTimelineClosed() {
|
||||
|
@ -81,7 +105,7 @@ export class RoomEncryption {
|
|||
} else if (source === DecryptionSource.Retry) {
|
||||
// when retrying, we could have mixed events from at the bottom of the timeline (sync)
|
||||
// and somewhere else, so create a custom cache we use just for this operation.
|
||||
customCache = this._megolmEncryption.createSessionCache();
|
||||
customCache = this._megolmDecryption.createSessionCache();
|
||||
sessionCache = customCache;
|
||||
} else {
|
||||
throw new Error("Unknown source: " + source);
|
||||
|
@ -91,13 +115,13 @@ export class RoomEncryption {
|
|||
if (customCache) {
|
||||
customCache.dispose();
|
||||
}
|
||||
return new DecryptionPreparation(preparation, errors, {isTimelineOpen}, this);
|
||||
return new DecryptionPreparation(preparation, errors, {isTimelineOpen, source}, this);
|
||||
}
|
||||
|
||||
async _processDecryptionResults(results, errors, flags, txn) {
|
||||
for (const error of errors.values()) {
|
||||
if (error.code === "MEGOLM_NO_SESSION") {
|
||||
this._addMissingSessionEvent(error.event);
|
||||
this._addMissingSessionEvent(error.event, flags.source);
|
||||
}
|
||||
}
|
||||
if (flags.isTimelineOpen) {
|
||||
|
@ -120,23 +144,78 @@ export class RoomEncryption {
|
|||
}
|
||||
}
|
||||
|
||||
_addMissingSessionEvent(event) {
|
||||
_addMissingSessionEvent(event, source) {
|
||||
const senderKey = event.content?.["sender_key"];
|
||||
const sessionId = event.content?.["session_id"];
|
||||
const key = `${senderKey}|${sessionId}`;
|
||||
const key = encodeMissingSessionKey(senderKey, sessionId);
|
||||
let eventIds = this._eventIdsByMissingSession.get(key);
|
||||
// new missing session
|
||||
if (!eventIds) {
|
||||
this._requestMissingSessionFromBackup(senderKey, sessionId, source);
|
||||
eventIds = new Set();
|
||||
this._eventIdsByMissingSession.set(key, eventIds);
|
||||
}
|
||||
eventIds.add(event.event_id);
|
||||
}
|
||||
|
||||
async _requestMissingSessionFromBackup(senderKey, sessionId, source) {
|
||||
if (!this._sessionBackup) {
|
||||
this._notifyMissingMegolmSession();
|
||||
return;
|
||||
}
|
||||
// if the message came from sync, wait 10s to see if the room key arrives,
|
||||
// and only after that proceed to request from backup
|
||||
if (source === DecryptionSource.Sync) {
|
||||
await this._clock.createTimeout(10000).elapsed();
|
||||
if (this._disposed || !this._eventIdsByMissingSession.has(encodeMissingSessionKey(senderKey, sessionId))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await this._sessionBackup.getSession(this._room.id, sessionId);
|
||||
if (session?.algorithm === MEGOLM_ALGORITHM) {
|
||||
if (session["sender_key"] !== senderKey) {
|
||||
console.warn("Got session key back from backup with different sender key, ignoring", {session, senderKey});
|
||||
return;
|
||||
}
|
||||
const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]);
|
||||
let roomKey;
|
||||
try {
|
||||
roomKey = await this._megolmDecryption.addRoomKeyFromBackup(
|
||||
this._room.id, sessionId, session, txn);
|
||||
} catch (err) {
|
||||
txn.abort();
|
||||
throw err;
|
||||
}
|
||||
await txn.complete();
|
||||
|
||||
if (roomKey) {
|
||||
// this will call into applyRoomKeys below
|
||||
await this._room.notifyRoomKeys([roomKey]);
|
||||
}
|
||||
} else if (session?.algorithm) {
|
||||
console.info(`Backed-up session of unknown algorithm: ${session.algorithm}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Could not get session ${sessionId} from backup`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {RoomKeyDescription}
|
||||
* @property {RoomKeyDescription} senderKey the curve25519 key of the sender
|
||||
* @property {RoomKeyDescription} sessionId
|
||||
*
|
||||
*
|
||||
* @param {Array<RoomKeyDescription>} roomKeys
|
||||
* @return {Array<string>} the event ids that should be retried to decrypt
|
||||
*/
|
||||
applyRoomKeys(roomKeys) {
|
||||
// retry decryption with the new sessions
|
||||
const retryEventIds = [];
|
||||
for (const roomKey of roomKeys) {
|
||||
const key = `${roomKey.senderKey}|${roomKey.sessionId}`;
|
||||
const key = encodeMissingSessionKey(roomKey.senderKey, roomKey.sessionId);
|
||||
const entriesForSession = this._eventIdsByMissingSession.get(key);
|
||||
if (entriesForSession) {
|
||||
this._eventIdsByMissingSession.delete(key);
|
||||
|
@ -263,6 +342,10 @@ export class RoomEncryption {
|
|||
const txnId = makeTxnId();
|
||||
await hsApi.sendToDevice(type, payload, txnId).response();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import anotherjson from "../../../lib/another-json/index.js";
|
||||
import {createEnum} from "../../utils/enum.js";
|
||||
|
||||
export const DecryptionSource = createEnum(["Sync", "Timeline", "Retry"]);
|
||||
export const DecryptionSource = createEnum("Sync", "Timeline", "Retry");
|
||||
|
||||
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
|
||||
export const SESSION_KEY_PREFIX = "e2ee:";
|
||||
|
|
|
@ -141,7 +141,53 @@ export class Decryption {
|
|||
const session = new this._olm.InboundGroupSession();
|
||||
try {
|
||||
session.create(sessionKey);
|
||||
const sessionEntry = await this._writeInboundSession(
|
||||
session, roomId, senderKey, claimedEd25519Key, sessionId, txn);
|
||||
if (sessionEntry) {
|
||||
newSessions.push(sessionEntry);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
}
|
||||
// this will be passed to the Room in notifyRoomKeys
|
||||
return newSessions;
|
||||
}
|
||||
|
||||
/*
|
||||
sessionInfo is a response from key backup and has the following keys:
|
||||
algorithm
|
||||
forwarding_curve25519_key_chain
|
||||
sender_claimed_keys
|
||||
sender_key
|
||||
session_key
|
||||
*/
|
||||
async addRoomKeyFromBackup(roomId, sessionId, sessionInfo, txn) {
|
||||
const sessionKey = sessionInfo["session_key"];
|
||||
const senderKey = sessionInfo["sender_key"];
|
||||
// TODO: can we just trust this?
|
||||
const claimedEd25519Key = sessionInfo["sender_claimed_keys"]?.["ed25519"];
|
||||
|
||||
if (
|
||||
typeof roomId !== "string" ||
|
||||
typeof sessionId !== "string" ||
|
||||
typeof senderKey !== "string" ||
|
||||
typeof sessionKey !== "string" ||
|
||||
typeof claimedEd25519Key !== "string"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const session = new this._olm.InboundGroupSession();
|
||||
try {
|
||||
session.import_session(sessionKey);
|
||||
return await this._writeInboundSession(
|
||||
session, roomId, senderKey, claimedEd25519Key, sessionId, txn);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
}
|
||||
|
||||
async _writeInboundSession(session, roomId, senderKey, claimedEd25519Key, sessionId, txn) {
|
||||
let incomingSessionIsBetter = true;
|
||||
const existingSessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
|
||||
if (existingSessionEntry) {
|
||||
|
@ -163,15 +209,8 @@ export class Decryption {
|
|||
claimedKeys: {ed25519: claimedEd25519Key},
|
||||
};
|
||||
txn.inboundGroupSessions.set(sessionEntry);
|
||||
newSessions.push(sessionEntry);
|
||||
return sessionEntry;
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
|
||||
}
|
||||
// this will be passed to the Room in notifyRoomKeys
|
||||
return newSessions;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
60
src/matrix/e2ee/megolm/SessionBackup.js
Normal file
60
src/matrix/e2ee/megolm/SessionBackup.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import base64 from "../../../../lib/base64-arraybuffer/index.js";
|
||||
|
||||
export class SessionBackup {
|
||||
constructor({backupInfo, decryption, hsApi}) {
|
||||
this._backupInfo = backupInfo;
|
||||
this._decryption = decryption;
|
||||
this._hsApi = hsApi;
|
||||
}
|
||||
|
||||
async getSession(roomId, sessionId) {
|
||||
const sessionResponse = await this._hsApi.roomKeyForRoomAndSession(this._backupInfo.version, roomId, sessionId).response();
|
||||
const sessionInfo = this._decryption.decrypt(
|
||||
sessionResponse.session_data.ephemeral,
|
||||
sessionResponse.session_data.mac,
|
||||
sessionResponse.session_data.ciphertext,
|
||||
);
|
||||
return JSON.parse(sessionInfo);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._decryption.free();
|
||||
}
|
||||
|
||||
static async fromSecretStorage({olm, secretStorage, hsApi, txn}) {
|
||||
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
|
||||
if (base64PrivateKey) {
|
||||
const privateKey = new Uint8Array(base64.decode(base64PrivateKey));
|
||||
const backupInfo = await hsApi.roomKeysVersion().response();
|
||||
const expectedPubKey = backupInfo.auth_data.public_key;
|
||||
const decryption = new olm.PkDecryption();
|
||||
try {
|
||||
const pubKey = decryption.init_with_private_key(privateKey);
|
||||
if (pubKey !== expectedPubKey) {
|
||||
throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`);
|
||||
}
|
||||
} catch(err) {
|
||||
decryption.free();
|
||||
throw err;
|
||||
}
|
||||
return new SessionBackup({backupInfo, decryption, hsApi});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,13 +73,13 @@ export class HomeServerApi {
|
|||
return `${this._homeserver}/_matrix/client/r0${csPath}`;
|
||||
}
|
||||
|
||||
_request(method, url, queryParams, body, options) {
|
||||
_baseRequest(method, url, queryParams, body, options, accessToken) {
|
||||
const queryString = encodeQueryParams(queryParams);
|
||||
url = `${url}?${queryString}`;
|
||||
let bodyString;
|
||||
const headers = new Map();
|
||||
if (this._accessToken) {
|
||||
headers.set("Authorization", `Bearer ${this._accessToken}`);
|
||||
if (accessToken) {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
headers.set("Accept", "application/json");
|
||||
if (body) {
|
||||
|
@ -106,16 +106,24 @@ export class HomeServerApi {
|
|||
return wrapper;
|
||||
}
|
||||
|
||||
_unauthedRequest(method, url, queryParams, body, options) {
|
||||
return this._baseRequest(method, url, queryParams, body, options, null);
|
||||
}
|
||||
|
||||
_authedRequest(method, url, queryParams, body, options) {
|
||||
return this._baseRequest(method, url, queryParams, body, options, this._accessToken);
|
||||
}
|
||||
|
||||
_post(csPath, queryParams, body, options) {
|
||||
return this._request("POST", this._url(csPath), queryParams, body, options);
|
||||
return this._authedRequest("POST", this._url(csPath), queryParams, body, options);
|
||||
}
|
||||
|
||||
_put(csPath, queryParams, body, options) {
|
||||
return this._request("PUT", this._url(csPath), queryParams, body, options);
|
||||
return this._authedRequest("PUT", this._url(csPath), queryParams, body, options);
|
||||
}
|
||||
|
||||
_get(csPath, queryParams, body, options) {
|
||||
return this._request("GET", this._url(csPath), queryParams, body, options);
|
||||
return this._authedRequest("GET", this._url(csPath), queryParams, body, options);
|
||||
}
|
||||
|
||||
sync(since, filter, timeout, options = null) {
|
||||
|
@ -142,7 +150,7 @@ export class HomeServerApi {
|
|||
}
|
||||
|
||||
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
||||
return this._post("/login", null, {
|
||||
return this._unauthedRequest("POST", this._url("/login"), null, {
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
|
@ -158,7 +166,7 @@ export class HomeServerApi {
|
|||
}
|
||||
|
||||
versions(options = null) {
|
||||
return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
||||
return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
||||
}
|
||||
|
||||
uploadKeys(payload, options = null) {
|
||||
|
@ -177,6 +185,18 @@ export class HomeServerApi {
|
|||
return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options);
|
||||
}
|
||||
|
||||
roomKeysVersion(version = null, options = null) {
|
||||
let versionPart = "";
|
||||
if (version) {
|
||||
versionPart = `/${encodeURIComponent(version)}`;
|
||||
}
|
||||
return this._get(`/room_keys/version${versionPart}`, null, null, options);
|
||||
}
|
||||
|
||||
roomKeyForRoomAndSession(version, roomId, sessionId, options = null) {
|
||||
return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options);
|
||||
}
|
||||
|
||||
get mediaRepository() {
|
||||
return this._mediaRepository;
|
||||
}
|
||||
|
|
|
@ -429,6 +429,10 @@ export class Room extends EventEmitter {
|
|||
return !!this._summary.encryption;
|
||||
}
|
||||
|
||||
enableSessionBackup(sessionBackup) {
|
||||
this._roomEncryption?.enableSessionBackup(sessionBackup);
|
||||
}
|
||||
|
||||
get isTrackingMembers() {
|
||||
return this._summary.isTrackingMembers;
|
||||
}
|
||||
|
@ -525,6 +529,11 @@ export class Room extends EventEmitter {
|
|||
applyIsTrackingMembersChanges(changes) {
|
||||
this._summary.applyChanges(changes);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._roomEncryption?.dispose();
|
||||
this._timeline?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class DecryptionRequest {
|
||||
|
|
|
@ -326,7 +326,7 @@ export class RoomSummary {
|
|||
* writeIsTrackingMembers, ... take a txn directly.
|
||||
*/
|
||||
async writeAndApplyChanges(data, storage) {
|
||||
const txn = await storage.readTxn([
|
||||
const txn = await storage.readWriteTxn([
|
||||
storage.storeNames.roomSummary,
|
||||
]);
|
||||
try {
|
||||
|
|
72
src/matrix/ssss/SecretStorage.js
Normal file
72
src/matrix/ssss/SecretStorage.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import base64 from "../../../lib/base64-arraybuffer/index.js";
|
||||
|
||||
export class SecretStorage {
|
||||
constructor({key, cryptoDriver}) {
|
||||
this._key = key;
|
||||
this._cryptoDriver = cryptoDriver;
|
||||
}
|
||||
|
||||
async readSecret(name, txn) {
|
||||
const accountData = await txn.accountData.get(name);
|
||||
if (!accountData) {
|
||||
return;
|
||||
}
|
||||
const encryptedData = accountData?.content?.encrypted?.[this._key.id];
|
||||
if (!encryptedData) {
|
||||
throw new Error(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`);
|
||||
}
|
||||
|
||||
if (this._key.algorithm === "m.secret_storage.v1.aes-hmac-sha2") {
|
||||
return await this._decryptAESSecret(accountData.type, encryptedData);
|
||||
} else {
|
||||
throw new Error(`Unsupported algorithm for key ${this._key.id}: ${this._key.algorithm}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _decryptAESSecret(type, encryptedData) {
|
||||
// TODO: we should we move this to platform specific code
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
// now derive the aes and mac key from the 4s key
|
||||
const hkdfKey = await this._cryptoDriver.derive.hkdf(
|
||||
this._key.binaryKey,
|
||||
new Uint8Array(8).buffer, //zero salt
|
||||
textEncoder.encode(type), // info
|
||||
"SHA-256",
|
||||
512 // 512 bits or 64 bytes
|
||||
);
|
||||
const aesKey = hkdfKey.slice(0, 32);
|
||||
const hmacKey = hkdfKey.slice(32);
|
||||
|
||||
const ciphertextBytes = base64.decode(encryptedData.ciphertext);
|
||||
|
||||
const isVerified = await this._cryptoDriver.hmac.verify(
|
||||
hmacKey, base64.decode(encryptedData.mac),
|
||||
ciphertextBytes, "SHA-256");
|
||||
|
||||
if (!isVerified) {
|
||||
throw new Error("Bad MAC");
|
||||
}
|
||||
|
||||
const plaintextBytes = await this._cryptoDriver.aes.decrypt(
|
||||
aesKey, base64.decode(encryptedData.iv), ciphertextBytes);
|
||||
|
||||
return textDecoder.decode(plaintextBytes);
|
||||
}
|
||||
}
|
53
src/matrix/ssss/common.js
Normal file
53
src/matrix/ssss/common.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export class KeyDescription {
|
||||
constructor(id, keyAccountData) {
|
||||
this._id = id;
|
||||
this._keyAccountData = keyAccountData;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get passphraseParams() {
|
||||
return this._keyAccountData?.content?.passphrase;
|
||||
}
|
||||
|
||||
get algorithm() {
|
||||
return this._keyAccountData?.content?.algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
export class Key {
|
||||
constructor(keyDescription, binaryKey) {
|
||||
this._keyDescription = keyDescription;
|
||||
this._binaryKey = binaryKey;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._keyDescription.id;
|
||||
}
|
||||
|
||||
get binaryKey() {
|
||||
return this._binaryKey;
|
||||
}
|
||||
|
||||
get algorithm() {
|
||||
return this._keyDescription.algorithm;
|
||||
}
|
||||
}
|
64
src/matrix/ssss/index.js
Normal file
64
src/matrix/ssss/index.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import {KeyDescription, Key} from "./common.js";
|
||||
import {keyFromPassphrase} from "./passphrase.js";
|
||||
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
||||
|
||||
async function readDefaultKeyDescription(storage) {
|
||||
const txn = await storage.readTxn([
|
||||
storage.storeNames.accountData
|
||||
]);
|
||||
const defaultKeyEvent = await txn.accountData.get("m.secret_storage.default_key");
|
||||
const id = defaultKeyEvent?.content?.key;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${id}`);
|
||||
if (!keyAccountData) {
|
||||
return;
|
||||
}
|
||||
return new KeyDescription(id, keyAccountData);
|
||||
}
|
||||
|
||||
export async function writeKey(key, txn) {
|
||||
txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
|
||||
}
|
||||
|
||||
export async function readKey(txn) {
|
||||
const keyData = await txn.session.get("ssssKey");
|
||||
if (!keyData) {
|
||||
return;
|
||||
}
|
||||
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`);
|
||||
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
|
||||
}
|
||||
|
||||
export async function keyFromCredential(type, credential, storage, cryptoDriver, olm) {
|
||||
const keyDescription = await readDefaultKeyDescription(storage);
|
||||
if (!keyDescription) {
|
||||
throw new Error("Could not find a default secret storage key in account data");
|
||||
}
|
||||
let key;
|
||||
if (type === "passphrase") {
|
||||
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver);
|
||||
} else if (type === "recoverykey") {
|
||||
key = keyFromRecoveryKey(olm, keyDescription, credential);
|
||||
} else {
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
return key;
|
||||
}
|
46
src/matrix/ssss/passphrase.js
Normal file
46
src/matrix/ssss/passphrase.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import {Key} from "./common.js";
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
const DEFAULT_BITSIZE = 256;
|
||||
|
||||
/**
|
||||
* @param {KeyDescription} keyDescription
|
||||
* @param {string} passphrase
|
||||
* @param {CryptoDriver} cryptoDriver
|
||||
* @return {Key}
|
||||
*/
|
||||
export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver) {
|
||||
const {passphraseParams} = keyDescription;
|
||||
if (!passphraseParams) {
|
||||
throw new Error("not a passphrase key");
|
||||
}
|
||||
if (passphraseParams.algorithm !== "m.pbkdf2") {
|
||||
throw new Error(`Unsupported passphrase algorithm: ${passphraseParams.algorithm}`);
|
||||
}
|
||||
// TODO: we should we move this to platform specific code
|
||||
const textEncoder = new TextEncoder();
|
||||
const keyBits = await cryptoDriver.derive.pbkdf2(
|
||||
textEncoder.encode(passphrase),
|
||||
passphraseParams.iterations || DEFAULT_ITERATIONS,
|
||||
// salt is just a random string, not encoded in any way
|
||||
textEncoder.encode(passphraseParams.salt),
|
||||
"SHA-512",
|
||||
passphraseParams.bits || DEFAULT_BITSIZE);
|
||||
return new Key(keyDescription, keyBits);
|
||||
}
|
57
src/matrix/ssss/recoveryKey.js
Normal file
57
src/matrix/ssss/recoveryKey.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
import bs58 from "../../../lib/bs58/index.js";
|
||||
import {Key} from "./common.js";
|
||||
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
/**
|
||||
* @param {Olm} olm
|
||||
* @param {KeyDescription} keyDescription
|
||||
* @param {string} recoveryKey
|
||||
* @return {Key}
|
||||
*/
|
||||
export function keyFromRecoveryKey(olm, keyDescription, recoveryKey) {
|
||||
const result = bs58.decode(recoveryKey.replace(/ /g, ''));
|
||||
|
||||
let parity = 0;
|
||||
for (const b of result) {
|
||||
parity ^= b;
|
||||
}
|
||||
if (parity !== 0) {
|
||||
throw new Error("Incorrect parity");
|
||||
}
|
||||
|
||||
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
|
||||
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||
throw new Error("Incorrect prefix");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.length !==
|
||||
OLM_RECOVERY_KEY_PREFIX.length + olm.PRIVATE_KEY_LENGTH + 1
|
||||
) {
|
||||
throw new Error("Incorrect length");
|
||||
}
|
||||
|
||||
const keyBits = Uint8Array.from(result.slice(
|
||||
OLM_RECOVERY_KEY_PREFIX.length,
|
||||
OLM_RECOVERY_KEY_PREFIX.length + olm.PRIVATE_KEY_LENGTH,
|
||||
));
|
||||
|
||||
return new Key(keyDescription, keyBits);
|
||||
}
|
|
@ -28,7 +28,8 @@ export const STORE_NAMES = Object.freeze([
|
|||
"inboundGroupSessions",
|
||||
"outboundGroupSessions",
|
||||
"groupSessionDecryptions",
|
||||
"operations"
|
||||
"operations",
|
||||
"accountData",
|
||||
]);
|
||||
|
||||
export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { openDatabase, reqAsPromise } from "./utils.js";
|
|||
import { exportSession, importSession } from "./export.js";
|
||||
import { schema } from "./schema.js";
|
||||
|
||||
const sessionName = sessionId => `brawl_session_${sessionId}`;
|
||||
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
||||
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length);
|
||||
|
||||
export class StorageFactory {
|
||||
|
|
|
@ -31,6 +31,7 @@ import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore.js";
|
|||
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore.js";
|
||||
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore.js";
|
||||
import {OperationStore} from "./stores/OperationStore.js";
|
||||
import {AccountDataStore} from "./stores/AccountDataStore.js";
|
||||
|
||||
export class Transaction {
|
||||
constructor(txn, allowedStoreNames) {
|
||||
|
@ -111,6 +112,10 @@ export class Transaction {
|
|||
return this._store("operations", idbStore => new OperationStore(idbStore));
|
||||
}
|
||||
|
||||
get accountData() {
|
||||
return this._store("accountData", idbStore => new AccountDataStore(idbStore));
|
||||
}
|
||||
|
||||
complete() {
|
||||
return txnAsPromise(this._txn);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ export const schema = [
|
|||
createMemberStore,
|
||||
migrateSession,
|
||||
createE2EEStores,
|
||||
migrateEncryptionFlag
|
||||
migrateEncryptionFlag,
|
||||
createAccountDataStore
|
||||
];
|
||||
// TODO: how to deal with git merge conflicts of this array?
|
||||
|
||||
|
@ -97,3 +98,8 @@ async function migrateEncryptionFlag(db, txn) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v6
|
||||
function createAccountDataStore(db) {
|
||||
db.createObjectStore("accountData", {keyPath: "type"});
|
||||
}
|
||||
|
|
29
src/matrix/storage/idb/stores/AccountDataStore.js
Normal file
29
src/matrix/storage/idb/stores/AccountDataStore.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
export class AccountDataStore {
|
||||
constructor(store) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
async get(type) {
|
||||
return await this._store.get(type);
|
||||
}
|
||||
|
||||
set(event) {
|
||||
return this._store.put(event);
|
||||
}
|
||||
}
|
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(new Uint8Array(key), new aesjs.Counter(new Uint8Array(iv)));
|
||||
return aesCtr.decrypt(new Uint8Array(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)}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ export class SessionStatusView extends TemplateView {
|
|||
spinner(t, {hidden: vm => !vm.isWaiting}),
|
||||
t.p(vm => vm.statusLabel),
|
||||
t.if(vm => vm.isConnectNowShown, t.createTemplate(t => t.button({onClick: () => vm.connectNow()}, "Retry now"))),
|
||||
t.if(vm => vm.isSecretStorageShown, t.createTemplate(t => t.button({onClick: () => vm.enterPassphrase(prompt("Security key"))}, "Enter security key"))),
|
||||
window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : ""
|
||||
]);
|
||||
}
|
||||
|
|
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;
|
||||
}
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
export function createEnum(...values) {
|
||||
const obj = {};
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Invalid enum value name" + value?.toString());
|
||||
}
|
||||
obj[value] = value;
|
||||
}
|
||||
return Object.freeze(obj);
|
||||
|
|
|
@ -21,3 +21,30 @@ import "regenerator-runtime/runtime";
|
|||
import "core-js/modules/es.promise";
|
||||
import "core-js/modules/es.math.imul";
|
||||
import "core-js/modules/es.math.clz32";
|
||||
|
||||
import "core-js/modules/es.typed-array.from";
|
||||
import "core-js/modules/es.typed-array.of";
|
||||
import "core-js/modules/es.typed-array.copy-within";
|
||||
import "core-js/modules/es.typed-array.every";
|
||||
import "core-js/modules/es.typed-array.fill";
|
||||
import "core-js/modules/es.typed-array.filter";
|
||||
import "core-js/modules/es.typed-array.find";
|
||||
import "core-js/modules/es.typed-array.find-index";
|
||||
import "core-js/modules/es.typed-array.for-each";
|
||||
import "core-js/modules/es.typed-array.includes";
|
||||
import "core-js/modules/es.typed-array.index-of";
|
||||
import "core-js/modules/es.typed-array.join";
|
||||
import "core-js/modules/es.typed-array.last-index-of";
|
||||
import "core-js/modules/es.typed-array.map";
|
||||
import "core-js/modules/es.typed-array.reduce";
|
||||
import "core-js/modules/es.typed-array.reduce-right";
|
||||
import "core-js/modules/es.typed-array.reverse";
|
||||
import "core-js/modules/es.typed-array.set";
|
||||
import "core-js/modules/es.typed-array.slice";
|
||||
import "core-js/modules/es.typed-array.some";
|
||||
import "core-js/modules/es.typed-array.sort";
|
||||
import "core-js/modules/es.typed-array.subarray";
|
||||
import "core-js/modules/es.typed-array.to-locale-string";
|
||||
import "core-js/modules/es.typed-array.to-string";
|
||||
import "core-js/modules/es.typed-array.iterator";
|
||||
import "core-js/modules/es.object.to-string";
|
||||
|
|
69
yarn.lock
69
yarn.lock
|
@ -821,7 +821,16 @@
|
|||
globals "^11.1.0"
|
||||
lodash "^4.17.19"
|
||||
|
||||
"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4":
|
||||
"@babel/types@^7.10.4":
|
||||
version "7.11.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
|
||||
integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.10.4"
|
||||
lodash "^4.17.19"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4":
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d"
|
||||
integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==
|
||||
|
@ -831,9 +840,9 @@
|
|||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@rollup/plugin-babel@^5.1.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.2.0.tgz#b87556d61ed108b4eaf9d18b5323965adf8d9bee"
|
||||
integrity sha512-CPABsajaKjINgBQ3it+yMnfVO3ibsrMBxRzbUOUw2cL1hsZJ7aogU8mgglQm3S2hHJgjnAmxPz0Rq7DVdmHsTw==
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.2.1.tgz#20fc8f8864dc0eaa1c5578408459606808f72924"
|
||||
integrity sha512-Jd7oqFR2dzZJ3NWANDyBjwTtX/lYbZpVcmkHrfQcpvawHs9E4c0nYk5U2mfZ6I/DZcIvy506KZJi54XK/jxH7A==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.10.4"
|
||||
"@rollup/pluginutils" "^3.1.0"
|
||||
|
@ -886,9 +895,9 @@
|
|||
picomatch "^2.2.2"
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.42"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11"
|
||||
integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==
|
||||
version "0.0.45"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
|
||||
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
|
@ -896,9 +905,9 @@
|
|||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/node@*":
|
||||
version "13.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72"
|
||||
integrity sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==
|
||||
version "14.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.10.3.tgz#5ae1f119c96643fc9b19b2d1a83bfa2ec3dbb7ea"
|
||||
integrity sha512-zdN0hor7TLkjAdKTnYW+Y22oIhUUpil5ZD1V1OFq0CR0CLKw+NdR6dkziTfkWRLo6sKzisayoj/GNpNbe4LY9Q==
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
|
@ -907,6 +916,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
aes-js@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
|
||||
integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
|
||||
|
||||
another-json@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc"
|
||||
|
@ -931,6 +945,18 @@ balanced-match@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base-x@^3.0.2:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d"
|
||||
integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
base64-arraybuffer@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
||||
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
|
||||
|
||||
boolbase@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
|
@ -954,6 +980,13 @@ browserslist@^4.12.0, browserslist@^4.8.5:
|
|||
escalade "^3.0.2"
|
||||
node-releases "^1.1.60"
|
||||
|
||||
bs58@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
|
||||
integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo=
|
||||
dependencies:
|
||||
base-x "^3.0.2"
|
||||
|
||||
builtin-modules@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
|
||||
|
@ -1720,12 +1753,17 @@ rollup-pluginutils@^2.3.3:
|
|||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^2.26.4:
|
||||
version "2.26.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.4.tgz#a8350fd6bd56fce9873a7db2bd9547d40de3992b"
|
||||
integrity sha512-6+qsGuP0MXGd7vlYmk72utm1MrgZj5GfXibGL+cRkKQ9+ZL/BnFThDl0D5bcl7AqlzMjAQXRAwZX1HVm22M/4Q==
|
||||
version "2.27.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.27.1.tgz#372744e1d36eba0fd942d997600c2fc2ca266305"
|
||||
integrity sha512-GiWHQvnmMgBktSpY/1+nrGpwPsTw4b9P28og2uedfeq4JZ16rzAmnQ5Pm/E0/BEmDNia1ZbY7+qu3nBgNa19Hg==
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
safe-buffer@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
@ -1826,6 +1864,11 @@ supports-color@^6.1.0:
|
|||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
text-encoding@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643"
|
||||
integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==
|
||||
|
||||
to-fast-properties@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||
|
|
Loading…
Reference in a new issue