Merge pull request #1272 from matrix-org/dbkr/symmetric-ssss-migrate
Migration to symmetric SSSS
This commit is contained in:
@@ -16,11 +16,20 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
@@ -51,9 +60,8 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const pubkey = decryption.generate_key();
|
||||
const privkey = decryption.get_private_key();
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signingKey = signing.generate_seed();
|
||||
@@ -69,7 +77,7 @@ describe("Secrets", function() {
|
||||
|
||||
const getKey = jest.fn(e => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', privkey];
|
||||
return ['abc', key];
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
@@ -100,8 +108,7 @@ describe("Secrets", function() {
|
||||
};
|
||||
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1,
|
||||
pubkey: pubkey,
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
@@ -112,11 +119,11 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(await secretStorage.isStored("foo")).toBe(false);
|
||||
expect(await secretStorage.isStored("foo")).toBeFalsy();
|
||||
|
||||
await secretStorage.store("foo", "bar", ["abc"]);
|
||||
|
||||
expect(await secretStorage.isStored("foo")).toBe(true);
|
||||
expect(await secretStorage.isStored("foo")).toBeTruthy();
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
@@ -149,6 +156,13 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
expect(Object.keys(e.keys)).toEqual([newKeyId]);
|
||||
return [newKeyId, key];
|
||||
});
|
||||
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -156,6 +170,7 @@ describe("Secrets", function() {
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -170,7 +185,7 @@ describe("Secrets", function() {
|
||||
alice.resetCrossSigningKeys();
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
@@ -252,11 +267,22 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
return [Object.keys(e.keys)[0], key];
|
||||
});
|
||||
|
||||
const bob = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
|
||||
+40
-4
@@ -217,8 +217,10 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* Args:
|
||||
* {object} keys Information about the keys:
|
||||
* {
|
||||
* <key name>: {
|
||||
* pubkey: {UInt8Array}
|
||||
* keys: {
|
||||
* <key name>: {
|
||||
* pubkey: {UInt8Array}
|
||||
* }, ...
|
||||
* }
|
||||
* }
|
||||
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
|
||||
@@ -1247,7 +1249,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
* @param {boolean} checkKey check if the secret is encrypted by a trusted
|
||||
* key
|
||||
*
|
||||
* @return {boolean} whether or not the secret is stored
|
||||
* @return {object?} map of key name to key info the secret is encrypted
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -1295,6 +1299,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
"bootstrapSecretStorage",
|
||||
"addSecretStorageKey",
|
||||
"hasSecretStorageKey",
|
||||
"secretStorageKeyNeedsUpgrade",
|
||||
"storeSecret",
|
||||
"getSecret",
|
||||
"isSecretStored",
|
||||
@@ -1581,7 +1586,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
|
||||
|
||||
/**
|
||||
* Check whether the key backup private key is stored in secret storage.
|
||||
* @return {Promise<boolean>} Whether the backup key is stored.
|
||||
* @return {Promise<object?>} map of key name to key info the secret is
|
||||
* encrypted with, or null if it is not present or not encrypted with a
|
||||
* trusted key
|
||||
*/
|
||||
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
|
||||
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
|
||||
@@ -1734,6 +1741,35 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the raw key for a key backup from the password
|
||||
* Used when migrating key backups into SSSS
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {string} password Passphrase
|
||||
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
|
||||
* @return {Promise<Buffer>} key backup key
|
||||
*/
|
||||
MatrixClient.prototype.keyBackupKeyFromPassword = function(
|
||||
password, backupInfo,
|
||||
) {
|
||||
return keyFromAuthData(backupInfo.auth_data, password);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the raw key for a key backup from the recovery key
|
||||
* Used when migrating key backups into SSSS
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {string} recoveryKey The recovery key
|
||||
* @return {Buffer} key backup key
|
||||
*/
|
||||
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
|
||||
return decodeRecoveryKey(recoveryKey);
|
||||
};
|
||||
|
||||
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
||||
|
||||
/**
|
||||
|
||||
@@ -141,14 +141,28 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* want to know this anyway...
|
||||
*
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
* @returns {boolean} Whether all private keys were found in storage
|
||||
* @returns {object} map of key name to key info the secret is encrypted
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStoredInSecretStorage(secretStorage) {
|
||||
let stored = true;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored =
|
||||
await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s) {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
return stored;
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
intersect(
|
||||
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
|
||||
);
|
||||
}
|
||||
return Object.keys(stored).length ? stored : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+135
-102
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
@@ -19,8 +19,13 @@ import {logger} from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import {pkVerify} from './olmlib';
|
||||
import {randomString} from '../randomstring';
|
||||
import {encryptAES, decryptAES} from './aes';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
// don't use curve25519 for writing data.
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519
|
||||
= "m.secret_storage.v1.curve25519-aes-sha2";
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
@@ -85,20 +90,12 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
switch (algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const { passphrase, pubkey } = opts;
|
||||
// Copies in public key details of the form generated by
|
||||
// the Crypto module's `createRecoveryKeyFromPassphrase`.
|
||||
if (passphrase && pubkey) {
|
||||
keyData.passphrase = passphrase;
|
||||
keyData.pubkey = pubkey;
|
||||
} else if (pubkey) {
|
||||
keyData.pubkey = pubkey;
|
||||
} else {
|
||||
keyData.pubkey = decryption.generate_key();
|
||||
if (opts.passphrase) {
|
||||
keyData.passphrase = opts.passphrase;
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
@@ -155,6 +152,28 @@ export class SecretStorage extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key information for a given ID.
|
||||
*
|
||||
* @param {string} [keyId = default key's ID] The ID of the key to check
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @returns {Array?} If the key was found, the return value is an array of
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
*/
|
||||
async getKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a key with a given ID.
|
||||
*
|
||||
@@ -163,16 +182,16 @@ export class SecretStorage extends EventEmitter {
|
||||
* @return {boolean} Whether we have the key.
|
||||
*/
|
||||
async hasKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
return !!(await this.getKey(keyId));
|
||||
}
|
||||
|
||||
async keyNeedsUpgrade(keyId) {
|
||||
const keyInfo = await this.getKey(keyId);
|
||||
if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,24 +226,13 @@ export class SecretStorage extends EventEmitter {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
|
||||
// check signature of key info
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
try {
|
||||
encryption.set_recipient_key(keyInfo.pubkey);
|
||||
encrypted[keyId] = encryption.encrypt(secret);
|
||||
} finally {
|
||||
encryption.free();
|
||||
}
|
||||
const keys = {[keyId]: keyInfo};
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -238,29 +246,6 @@ export class SecretStorage extends EventEmitter {
|
||||
await this._baseApis.setAccountData(name, {encrypted});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a secret defined to be the same as the given key.
|
||||
* No secret information will be stored, instead the secret will
|
||||
* be stored with a marker to say that the contents of the secret is
|
||||
* the value of the given key.
|
||||
* This is useful for migration from systems that predate SSSS such as
|
||||
* key backup.
|
||||
*
|
||||
* @param {string} name The name of the secret
|
||||
* @param {string} keyId The ID of the key whose value will be the
|
||||
* value of the secret
|
||||
* @returns {Promise} resolved when account data is saved
|
||||
*/
|
||||
storePassthrough(name, keyId) {
|
||||
return this._baseApis.setAccountData(name, {
|
||||
encrypted: {
|
||||
[keyId]: {
|
||||
passthrough: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary method to fix up existing accounts where secrets
|
||||
* are incorrectly stored without the 'encrypted' level
|
||||
@@ -317,7 +302,12 @@ export class SecretStorage extends EventEmitter {
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
if (
|
||||
keyInfo.pubkey && (
|
||||
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
|
||||
@@ -344,15 +334,9 @@ export class SecretStorage extends EventEmitter {
|
||||
// since we just want to return the key itself.
|
||||
if (encInfo.passthrough) return decryption.get_private_key();
|
||||
|
||||
// decrypt secret
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
return decryption.decrypt(
|
||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
||||
);
|
||||
}
|
||||
return await decryption.decrypt(encInfo);
|
||||
} finally {
|
||||
if (decryption) decryption.free();
|
||||
if (decryption && decryption.free) decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,22 +346,26 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {string} name the name of the secret
|
||||
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
|
||||
*
|
||||
* @return {boolean} whether or not the secret is stored
|
||||
* @return {object?} map of key name to key info the secret is encrypted
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStored(name, checkKey) {
|
||||
// check if secret exists
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) return false;
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
|
||||
const ret = {};
|
||||
|
||||
// check if secret is encrypted by a known/trusted secret and
|
||||
// encryption looks sane
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
@@ -385,32 +373,55 @@ export class SecretStorage extends EventEmitter {
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) return false;
|
||||
if (!keyInfo) continue;
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
if (checkKey) {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't actually need the decryption object if it's a passthrough
|
||||
// since we just want to return the key itself.
|
||||
if (encInfo.passthrough) return true;
|
||||
if (encInfo.passthrough) {
|
||||
try {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
} catch (e) {
|
||||
// not trusted, so move on to the next key
|
||||
continue;
|
||||
}
|
||||
ret[keyId] = keyInfo;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
ret[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
return true;
|
||||
if (checkKey) {
|
||||
try {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
} catch (e) {
|
||||
// not trusted, so move on to the next key
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ret[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return Object.keys(ret).length ? ret : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -607,26 +618,48 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubkey;
|
||||
try {
|
||||
pubkey = decryption.init_with_private_key(privateKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||
}
|
||||
if (pubkey !== keys[keyId].pubkey) {
|
||||
decryption.free();
|
||||
throw new Error(
|
||||
"getSecretStorageKey callback returned incorrect key",
|
||||
);
|
||||
}
|
||||
return [keyId, decryption];
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
const decryption = {
|
||||
encrypt: async function(secret) {
|
||||
return await encryptAES(secret, privateKey, name);
|
||||
},
|
||||
decrypt: async function(encInfo) {
|
||||
return await decryptAES(encInfo, privateKey, name);
|
||||
},
|
||||
};
|
||||
return [keyId, decryption];
|
||||
}
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
{
|
||||
const pkDecryption = new global.Olm.PkDecryption();
|
||||
let pubkey;
|
||||
try {
|
||||
pubkey = pkDecryption.init_with_private_key(privateKey);
|
||||
} catch (e) {
|
||||
pkDecryption.free();
|
||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
if (pubkey !== keys[keyId].pubkey) {
|
||||
pkDecryption.free();
|
||||
throw new Error(
|
||||
"getSecretStorageKey callback returned incorrect key",
|
||||
);
|
||||
}
|
||||
const decryption = {
|
||||
free: pkDecryption.free.bind(pkDecryption),
|
||||
decrypt: async function(encInfo) {
|
||||
return pkDecryption.decrypt(
|
||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
||||
);
|
||||
},
|
||||
// needed for passthrough
|
||||
get_private_key: pkDecryption.get_private_key.bind(pkDecryption),
|
||||
};
|
||||
return [keyId, decryption];
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
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 {getCrypto} from '../utils';
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
|
||||
// salt for HKDF, with 8 bytes of zeros
|
||||
const zerosalt = new Uint8Array(8);
|
||||
|
||||
/**
|
||||
* encrypt a string in Node.js
|
||||
*
|
||||
* @param {string} data the plaintext to encrypt
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function encryptNode(data, key, name) {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of iv is a price we have to pay.
|
||||
iv[8] &= 0x7f;
|
||||
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
|
||||
const ciphertext = cipher.update(data, "utf-8", "base64")
|
||||
+ cipher.final("base64");
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(ciphertext, "base64").digest("base64");
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: ciphertext,
|
||||
mac: hmac,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* decrypt a string in Node.js
|
||||
*
|
||||
* @param {object} data the encrypted data
|
||||
* @param {string} data.ciphertext the ciphertext in base64
|
||||
* @param {string} data.iv the initialization vector in base64
|
||||
* @param {string} data.mac the HMAC in base64
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptNode(data, key, name) {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(data.ciphertext, "base64").digest("base64");
|
||||
|
||||
if (hmac !== data.mac) {
|
||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||
}
|
||||
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-256-ctr", aesKey, decodeBase64(data.iv),
|
||||
);
|
||||
return decipher.update(data.ciphertext, "base64", "utf-8")
|
||||
+ decipher.final("utf-8");
|
||||
}
|
||||
|
||||
function deriveKeysNode(key, name) {
|
||||
const crypto = getCrypto();
|
||||
const prk = crypto.createHmac("sha256", zerosalt)
|
||||
.update(key).digest();
|
||||
|
||||
const b = Buffer.alloc(1, 1);
|
||||
const aesKey = crypto.createHmac("sha256", prk)
|
||||
.update(name, "utf-8").update(b).digest();
|
||||
b[0] = 2;
|
||||
const hmacKey = crypto.createHmac("sha256", prk)
|
||||
.update(aesKey).update(name, "utf-8").update(b).digest();
|
||||
|
||||
return [aesKey, hmacKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt a string in Node.js
|
||||
*
|
||||
* @param {string} data the plaintext to encrypt
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function encryptBrowser(data, key, name) {
|
||||
const iv = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(iv);
|
||||
|
||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of iv is a price we have to pay.
|
||||
iv[8] &= 0x7f;
|
||||
|
||||
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
|
||||
const encodedData = new TextEncoder().encode(data);
|
||||
|
||||
const ciphertext = await subtleCrypto.encrypt(
|
||||
{
|
||||
name: "AES-CTR",
|
||||
counter: iv,
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
encodedData,
|
||||
);
|
||||
|
||||
const hmac = await subtleCrypto.sign(
|
||||
{name: 'HMAC'},
|
||||
hmacKey,
|
||||
ciphertext,
|
||||
);
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
mac: encodeBase64(hmac),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* decrypt a string in the browser
|
||||
*
|
||||
* @param {object} data the encrypted data
|
||||
* @param {string} data.ciphertext the ciphertext in base64
|
||||
* @param {string} data.iv the initialization vector in base64
|
||||
* @param {string} data.mac the HMAC in base64
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptBrowser(data, key, name) {
|
||||
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
|
||||
|
||||
const ciphertext = decodeBase64(data.ciphertext);
|
||||
|
||||
if (!await subtleCrypto.verify(
|
||||
{name: "HMAC"},
|
||||
hmacKey,
|
||||
decodeBase64(data.mac),
|
||||
ciphertext,
|
||||
)) {
|
||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||
}
|
||||
|
||||
const plaintext = await subtleCrypto.decrypt(
|
||||
{
|
||||
name: "AES-CTR",
|
||||
counter: decodeBase64(data.iv),
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
ciphertext,
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
async function deriveKeysBrowser(key, name) {
|
||||
const hkdfkey = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{name: "HKDF"},
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
const keybits = await subtleCrypto.deriveBits(
|
||||
{
|
||||
name: "HKDF",
|
||||
salt: zerosalt,
|
||||
info: (new TextEncoder().encode(name)),
|
||||
hash: "SHA-256",
|
||||
},
|
||||
hkdfkey,
|
||||
512,
|
||||
);
|
||||
|
||||
const aesKey = keybits.slice(0, 32);
|
||||
const hmacKey = keybits.slice(32);
|
||||
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{name: 'AES-CTR'},
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
);
|
||||
|
||||
const hmacProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: {name: 'SHA-256'},
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
);
|
||||
|
||||
return await Promise.all([aesProm, hmacProm]);
|
||||
}
|
||||
|
||||
export function encryptAES(...args) {
|
||||
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
|
||||
}
|
||||
|
||||
export function decryptAES(...args) {
|
||||
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
|
||||
}
|
||||
|
||||
+143
-37
@@ -39,7 +39,7 @@ import {
|
||||
UserTrustLevel,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from './CrossSigning';
|
||||
import {SECRET_STORAGE_ALGORITHM_V1, SecretStorage} from './SecretStorage';
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
|
||||
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {
|
||||
@@ -371,6 +371,9 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
|
||||
* @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Buffer
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
* Returns:
|
||||
* {Promise} A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
|
||||
@@ -381,6 +384,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
getKeyBackupPassphrase,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
|
||||
@@ -395,10 +399,82 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
// use temporary callbacks to weave them through the various APIs.
|
||||
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
|
||||
|
||||
// the ID of the new SSSS key, if we create one
|
||||
let newKeyId = null;
|
||||
|
||||
// cache SSSS keys so that we don't need to constantly pester the user about it
|
||||
const ssssKeys = {};
|
||||
|
||||
this._baseApis._cryptoCallbacks.getSecretStorageKey =
|
||||
async ({keys}, name) => {
|
||||
// if we already have a key that works, return it
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (ssssKeys[keyId]) {
|
||||
return [keyId, ssssKeys[keyId]];
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, prompt the user and cache it
|
||||
const key = await appCallbacks.getSecretStorageKey({keys}, name);
|
||||
if (key) {
|
||||
const [keyId, keyData] = key;
|
||||
ssssKeys[keyId] = keyData;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
try {
|
||||
const inStorage = !setupNewSecretStorage &&
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
if (!this._crossSigningInfo.getId() || !inStorage) {
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
if (decryptionKeys && !(Object.values(decryptionKeys).some(
|
||||
info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
))) {
|
||||
// we already have cross-signing keys, but they're encrypted using
|
||||
// the old algorithm
|
||||
logger.log("Switching to symmetric");
|
||||
const keys = {};
|
||||
// fetch the cross-signing private keys (needed to sign the new
|
||||
// SSSS key). We store the cross-signing keys, and temporarily set
|
||||
// a callback so that when the private key is needed while setting
|
||||
// things up, we can provide it.
|
||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
||||
name => crossSigningPrivateKeys[name];
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const secretName = `m.cross_signing.${type}`;
|
||||
const secret = await this.getSecret(secretName);
|
||||
keys[type] = secret;
|
||||
crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret);
|
||||
}
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
const opts = {};
|
||||
let oldKeyId = null;
|
||||
for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) {
|
||||
// See if the old key was generated from a passphrase. If
|
||||
// yes, use the same settings.
|
||||
if (keyId in ssssKeys) {
|
||||
oldKeyId = keyId;
|
||||
if (keyInfo.passphrase) {
|
||||
opts.passphrase = keyInfo.passphrase;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// create new symmetric SSSS key and set it as default
|
||||
newKeyId = await this.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||
);
|
||||
if (oldKeyId) {
|
||||
ssssKeys[newKeyId] = ssssKeys[oldKeyId];
|
||||
}
|
||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
||||
// re-encrypt all the keys with the new key
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const secretName = `m.cross_signing.${type}`;
|
||||
await this.storeSecret(secretName, keys[type], [newKeyId]);
|
||||
}
|
||||
} else if (!this._crossSigningInfo.getId() || !inStorage) {
|
||||
// create new cross-signing keys if necessary.
|
||||
logger.log(
|
||||
"Cross-signing public and/or private keys not found, " +
|
||||
"checking secret storage for private keys",
|
||||
@@ -424,40 +500,50 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
logger.log("Cross signing keys are present in secret storage");
|
||||
}
|
||||
|
||||
// Check if Secure Secret Storage has a default key. If we don't have one, create
|
||||
// the default key (which will also be signed by the cross-signing master key).
|
||||
if (setupNewSecretStorage || !await this.hasSecretStorageKey()) {
|
||||
let newKeyId;
|
||||
// Check if we need to create a new secret storage key
|
||||
// - we're resetting secret storage
|
||||
// - we don't have a default secret storage key yet
|
||||
// - our default secret storage key is using an older algorithm
|
||||
// We will also run this part if we created a new secret storage key
|
||||
// above, so that we can (re-)encrypt the backup with it.
|
||||
const defaultSSSSKey = await this.getSecretStorageKey();
|
||||
if (setupNewSecretStorage || newKeyId || !defaultSSSSKey
|
||||
|| defaultSSSSKey[1].algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (keyBackupInfo) {
|
||||
// if we already have a backup key, use the same key as the
|
||||
// secret storage key
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
const opts = {
|
||||
pubkey: keyBackupInfo.auth_data.public_key,
|
||||
};
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
};
|
||||
const backupKey = await getKeyBackupPassphrase();
|
||||
|
||||
if (!newKeyId) {
|
||||
const opts = {};
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
bits: 256,
|
||||
};
|
||||
}
|
||||
|
||||
newKeyId = await this.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||
);
|
||||
this.setDefaultSecretStorageKeyId(newKeyId);
|
||||
// use the backup key as the new ssss key
|
||||
ssssKeys[newKeyId] = backupKey;
|
||||
}
|
||||
|
||||
newKeyId = await this.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1, opts,
|
||||
);
|
||||
|
||||
// Add an entry for the backup key in SSSS as a 'passthrough' key
|
||||
// (ie. the secret is the key itself).
|
||||
this._secretStorage.storePassthrough('m.megolm_backup.v1', newKeyId);
|
||||
|
||||
// if this key backup is trusted, sign it with the cross signing key
|
||||
// so the key backup can be trusted via cross-signing.
|
||||
const backupSigStatus = await this.checkKeyBackup(keyBackupInfo);
|
||||
if (backupSigStatus.trustInfo.usable) {
|
||||
console.log("Adding cross signing signature to key backup");
|
||||
logger.log("Adding cross signing signature to key backup");
|
||||
await this._crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
@@ -466,20 +552,32 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
undefined, keyBackupInfo,
|
||||
{prefix: httpApi.PREFIX_UNSTABLE},
|
||||
);
|
||||
await this.storeSecret(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Key backup is NOT TRUSTED: NOT adding cross signing signature",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.log("Secret storage default key not found, creating new key");
|
||||
const keyOptions = await createSecretStorageKey();
|
||||
newKeyId = await this.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1,
|
||||
keyOptions,
|
||||
);
|
||||
if (!newKeyId) {
|
||||
logger.log("Secret storage default key not found, creating new key");
|
||||
const keyOptions = await createSecretStorageKey();
|
||||
newKeyId = await this.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
keyOptions,
|
||||
);
|
||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
||||
}
|
||||
if (await this.isSecretStored("m.megolm_backup.v1")) {
|
||||
// we created a new SSSS, and we previously encrypted the
|
||||
// backup key with the old SSSS key, so re-encrypt with the
|
||||
// new key
|
||||
const backupKey = await this.getSecret("m.megolm_backup.v1");
|
||||
await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]);
|
||||
}
|
||||
}
|
||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
||||
} else {
|
||||
logger.log("Have secret storage key");
|
||||
}
|
||||
@@ -535,6 +633,14 @@ Crypto.prototype.hasSecretStorageKey = function(keyID) {
|
||||
return this._secretStorage.hasKey(keyID);
|
||||
};
|
||||
|
||||
Crypto.prototype.secretStorageKeyNeedsUpgrade = function(keyID) {
|
||||
return this._secretStorage.keyNeedsUpgrade(keyID);
|
||||
};
|
||||
|
||||
Crypto.prototype.getSecretStorageKey = function(keyID) {
|
||||
return this._secretStorage.getKey(keyID);
|
||||
};
|
||||
|
||||
Crypto.prototype.storeSecret = function(name, secret, keys) {
|
||||
return this._secretStorage.store(name, secret, keys);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ import {randomString} from '../randomstring';
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
const DEFAULT_BITSIZE = 256;
|
||||
|
||||
export async function keyFromAuthData(authData, password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
@@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) {
|
||||
return await deriveKey(
|
||||
password, authData.private_key_salt,
|
||||
authData.private_key_iterations,
|
||||
authData.private_key_bits || DEFAULT_BITSIZE,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) {
|
||||
|
||||
const salt = randomString(32);
|
||||
|
||||
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
|
||||
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
|
||||
|
||||
return { key, salt, iterations: DEFAULT_ITERATIONS };
|
||||
}
|
||||
|
||||
export async function deriveKey(password, salt, iterations) {
|
||||
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
|
||||
const subtleCrypto = global.crypto.subtle;
|
||||
const TextEncoder = global.TextEncoder;
|
||||
if (!subtleCrypto || !TextEncoder) {
|
||||
@@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) {
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
global.Olm.PRIVATE_KEY_LENGTH * 8,
|
||||
numBits,
|
||||
);
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
|
||||
@@ -21,5 +21,12 @@ import request from "request";
|
||||
matrixcs.request(request);
|
||||
utils.runPolyfills();
|
||||
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
export * from "./matrix";
|
||||
export default matrixcs;
|
||||
|
||||
@@ -734,3 +734,17 @@ export async function promiseMapSeries<T>(
|
||||
export function promiseTry<T>(fn: () => T): Promise<T> {
|
||||
return new Promise((resolve) => resolve(fn()));
|
||||
}
|
||||
|
||||
// We need to be able to access the Node.js crypto library from within the
|
||||
// Matrix SDK without needing to `require("crypto")`, which will fail in
|
||||
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
||||
// it, we can call `getCrypto`.
|
||||
let crypto: Object;
|
||||
|
||||
export function setCrypto(c: Object) {
|
||||
crypto = c;
|
||||
}
|
||||
|
||||
export function getCrypto(): Object {
|
||||
return crypto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user