index : matrix-js-sdk

My fork of matrix-js-sdk

diff options
context:
space:
mode:
authorBruno Windels <[email protected]>2020-06-18 14:29:35 +0000
committerGitHub <[email protected]>2020-06-18 14:29:35 +0000
commit8a6cd48b8ec57df2b87deca9ac90a6ad9533db1f (patch)
tree342be49c9e4b3126719820543a7dfef2515cb263
parente21d1f539d8783156ddf3b6cdf05816362752971 (diff)
parentf62049559cfa0320344c3236568f2b8c3c281751 (diff)
downloadmatrix-js-sdk-8a6cd48b8ec57df2b87deca9ac90a6ad9533db1f.tar.gz
Merge pull request #1380 from matrix-org/bwindels/bootstrap-operation
Isolate encryption bootstrap side-effects
-rw-r--r--spec/unit/crypto/backup.spec.js3
-rw-r--r--spec/unit/crypto/cross-signing.spec.js22
-rw-r--r--spec/unit/crypto/crypto-utils.js44
-rw-r--r--spec/unit/crypto/secrets.spec.js21
-rw-r--r--spec/unit/crypto/verification/sas.spec.js5
-rw-r--r--src/client.js12
-rw-r--r--src/crypto/CrossSigning.js6
-rw-r--r--src/crypto/EncryptionSetup.js326
-rw-r--r--src/crypto/index.js440
9 files changed, 599 insertions, 280 deletions
diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js
index e03460fe..34766b87 100644
--- a/spec/unit/crypto/backup.spec.js
+++ b/spec/unit/crypto/backup.spec.js
@@ -27,6 +27,7 @@ import {MockStorageApi} from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import {OlmDevice} from "../../../src/crypto/OlmDevice";
import {Crypto} from "../../../src/crypto";
+import {resetCrossSigningKeys} from "./crypto-utils";
const Olm = global.Olm;
@@ -332,7 +333,7 @@ describe("MegolmBackup", function() {
client.on("crossSigning.getKey", function(e) {
e.done(privateKeys[e.type]);
});
- await client.resetCrossSigningKeys();
+ await resetCrossSigningKeys(client);
let numCalls = 0;
await new Promise((resolve, reject) => {
client._http.authedRequest = function(
diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js
index ccdbef0b..7f24d1c7 100644
--- a/spec/unit/crypto/cross-signing.spec.js
+++ b/spec/unit/crypto/cross-signing.spec.js
@@ -20,6 +20,7 @@ import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import {TestClient} from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
+import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -66,8 +67,13 @@ describe("Cross Signing", function() {
);
});
alice.uploadKeySignatures = async () => {};
+ alice.setAccountData = async () => {};
+ alice.getAccountDataFromServer = async () => {};
// set Alice's cross-signing key
- await alice.resetCrossSigningKeys();
+ await alice.bootstrapSecretStorage({
+ createSecretStorageKey,
+ authUploadDeviceSigningKeys: async func => await func({}),
+ });
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
});
@@ -78,7 +84,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
@@ -273,7 +279,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
@@ -363,7 +369,7 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
@@ -520,7 +526,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
const bobMasterSigning = new global.Olm.PkSigning();
@@ -588,7 +594,7 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
@@ -740,7 +746,7 @@ describe("Cross Signing", function() {
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key
- await bob.resetCrossSigningKeys();
+ await resetCrossSigningKeys(bob);
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
@@ -766,7 +772,7 @@ describe("Cross Signing", function() {
let upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
- await alice.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice);
await upgradePromise;
const bobTrust = alice.checkUserTrust("@bob:example.com");
diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js
new file mode 100644
index 00000000..b54802b0
--- /dev/null
+++ b/spec/unit/crypto/crypto-utils.js
@@ -0,0 +1,44 @@
+import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
+
+
+// needs to be phased out and replaced with bootstrapSecretStorage,
+// but that is doing too much extra stuff for it to be an easy transition.
+export async function resetCrossSigningKeys(client, {
+ level,
+ authUploadDeviceSigningKeys = async func => await func(),
+} = {}) {
+ const crypto = client._crypto;
+
+ const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
+ try {
+ await crypto._crossSigningInfo.resetKeys(level);
+ await crypto._signObject(crypto._crossSigningInfo.keys.master);
+ // write a copy locally so we know these are trusted keys
+ await crypto._cryptoStore.doTxn(
+ 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
+ (txn) => {
+ crypto._cryptoStore.storeCrossSigningKeys(
+ txn, crypto._crossSigningInfo.keys);
+ },
+ );
+ } catch (e) {
+ // If anything failed here, revert the keys so we know to try again from the start
+ // next time.
+ crypto._crossSigningInfo.keys = oldKeys;
+ throw e;
+ }
+ crypto._baseApis.emit("crossSigning.keysChanged", {});
+ await crypto._afterCrossSigningLocalKeyChange();
+}
+
+export async function createSecretStorageKey() {
+ const decryption = new global.Olm.PkDecryption();
+ const storagePublicKey = decryption.generate_key();
+ const storagePrivateKey = decryption.get_private_key();
+ decryption.free();
+ return {
+ // `pubkey` not used anymore with symmetric 4S
+ keyInfo: { pubkey: storagePublicKey },
+ privateKey: storagePrivateKey,
+ };
+}
diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js
index d60ca1ba..ceb01e71 100644
--- a/spec/unit/crypto/secrets.spec.js
+++ b/spec/unit/crypto/secrets.spec.js
@@ -21,6 +21,7 @@ import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import {encryptAES} from "../../../src/crypto/aes";
+import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
import * as utils from "../../../src/utils";
@@ -190,7 +191,7 @@ describe("Secrets", function() {
}),
]);
};
- alice.resetCrossSigningKeys();
+ resetCrossSigningKeys(alice);
const newKeyId = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
@@ -325,7 +326,10 @@ describe("Secrets", function() {
this.emit("accountData", event);
};
- await bob.bootstrapSecretStorage();
+ await bob.bootstrapSecretStorage({
+ createSecretStorageKey,
+ authUploadDeviceSigningKeys: async func => await func({}),
+ });
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
@@ -381,6 +385,7 @@ describe("Secrets", function() {
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
}),
+ authUploadDeviceSigningKeys: async func => await func({}),
});
// Clear local cross-signing keys and read from secret storage
@@ -389,7 +394,9 @@ describe("Secrets", function() {
crossSigning.toStorage(),
);
crossSigning.keys = {};
- await bob.bootstrapSecretStorage();
+ await bob.bootstrapSecretStorage({
+ authUploadDeviceSigningKeys: async func => await func({}),
+ });
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@@ -510,7 +517,9 @@ describe("Secrets", function() {
this.emit("accountData", event);
};
- await alice.bootstrapSecretStorage();
+ await alice.bootstrapSecretStorage({
+ authUploadDeviceSigningKeys: async func => await func({}),
+ });
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
.toEqual({key: "key_id"});
@@ -650,7 +659,9 @@ describe("Secrets", function() {
this.emit("accountData", event);
};
- await alice.bootstrapSecretStorage();
+ await alice.bootstrapSecretStorage({
+ authUploadDeviceSigningKeys: async func => await func({}),
+ });
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js
index 88f424fe..60771231 100644
--- a/spec/unit/crypto/verification/sas.spec.js
+++ b/spec/unit/crypto/verification/sas.spec.js
@@ -22,6 +22,7 @@ import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
import {verificationMethods} from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import {logger} from "../../../../src/logger";
+import {resetCrossSigningKeys} from "../crypto-utils";
const Olm = global.Olm;
@@ -288,12 +289,12 @@ describe("SAS verification", function() {
);
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
alice.httpBackend.flush(undefined, 2);
- await alice.client.resetCrossSigningKeys();
+ await resetCrossSigningKeys(alice.client);
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
bob.httpBackend.flush(undefined, 2);
- await bob.client.resetCrossSigningKeys();
+ await resetCrossSigningKeys(bob.client);
bob.client._crypto._deviceList.storeCrossSigningForUser(
"@alice:example.com", {
diff --git a/src/client.js b/src/client.js
index 4ae6cfb0..0436e94d 100644
--- a/src/client.js
+++ b/src/client.js
@@ -1089,17 +1089,6 @@ function wrapCryptoFuncs(MatrixClient, names) {
}
}
- /**
- * Generate new cross-signing keys.
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @function module:client~MatrixClient#resetCrossSigningKeys
- * @param {object} authDict Auth data to supply for User-Interactive auth.
- * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
- * keys will be created for the given level and below. Defaults to
- * regenerating all keys.
- */
-
/**
* Get the user's cross-signing key ID.
* The cross-signing API is currently UNSTABLE and may change without notice.
@@ -1169,7 +1158,6 @@ function wrapCryptoFuncs(MatrixClient, names) {
* @param {module:models/room} room the room the event is in
*/
wrapCryptoFuncs(MatrixClient, [
- "resetCrossSigningKeys",
"getCrossSigningId",
"getStoredCrossSigningForUser",
"checkUserTrust",
diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js
index ff5bc708..ec82a604 100644
--- a/src/crypto/CrossSigning.js
+++ b/src/crypto/CrossSigning.js
@@ -178,12 +178,12 @@ export class CrossSigningInfo extends EventEmitter {
* typically called in conjunction with the creation of new cross-signing
* keys.
*
- * @param {object} keys The keys to store
+ * @param {Map} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data
*/
static async storeInSecretStorage(keys, secretStorage) {
- for (const type of Object.keys(keys)) {
- const encodedKey = encodeBase64(keys[type]);
+ for (const [type, privateKey] of keys) {
+ const encodedKey = encodeBase64(privateKey);
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
}
}
diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.js
new file mode 100644
index 00000000..549ec5e3
--- /dev/null
+++ b/src/crypto/EncryptionSetup.js
@@ -0,0 +1,326 @@
+import {MatrixEvent} from "../models/event";
+import {EventEmitter} from "events";
+import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
+import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
+import {
+ PREFIX_UNSTABLE,
+} from "../http-api";
+
+/**
+ * Builds an EncryptionSetupOperation by calling any of the add.. methods.
+ * Once done, `buildOperation()` can be called which allows to apply to operation.
+ *
+ * This is used as a helper by Crypto to keep track of all the network requests
+ * and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
+ * Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
+ * more than once.
+ */
+export class EncryptionSetupBuilder {
+ /**
+ * @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
+ */
+ constructor(accountData) {
+ this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
+ this.crossSigningCallbacks = new CrossSigningCallbacks();
+ this.ssssCryptoCallbacks = new SSSSCryptoCallbacks();
+
+ this._crossSigningKeys = null;
+ this._keySignatures = null;
+ this._keyBackupInfo = null;
+ }
+
+ /**
+ * Adds new cross-signing public keys
+ * @param {Object} auth auth dictionary needed to upload the new keys
+ * @param {Object} keys the new keys
+ */
+ addCrossSigningKeys(auth, keys) {
+ this._crossSigningKeys = {auth, keys};
+ }
+
+ /**
+ * Adds the key backup info to be updated on the server
+ *
+ * Used either to create a new key backup, or add signatures
+ * from the new MSK.
+ *
+ * @param {Object} keyBackupInfo as received from/sent to the server
+ */
+ addSessionBackup(keyBackupInfo) {
+ this._keyBackupInfo = keyBackupInfo;
+ }
+
+ /**
+ * Adds the session backup private key to be updated in the local cache
+ *
+ * Used after fixing the format of the key
+ *
+ * @param {Uint8Array} privateKey
+ */
+ addSessionBackupPrivateKeyToCache(privateKey) {
+ this._sessionBackupPrivateKey = privateKey;
+ }
+
+ /**
+ * Add signatures from a given user and device/x-sign key
+ * Used to sign the new cross-signing key with the device key
+ *
+ * @param {String} userId
+ * @param {String} deviceId
+ * @param {String} signature
+ */
+ addKeySignature(userId, deviceId, signature) {
+ if (!this._keySignatures) {
+ this._keySignatures = {};
+ }
+ const userSignatures = this._keySignatures[userId] || {};
+ this._keySignatures[userId] = userSignatures;
+ userSignatures[deviceId] = signature;
+ }
+
+
+ /**
+ * @param {String} type
+ * @param {Object} content
+ * @return {Promise}
+ */
+ setAccountData(type, content) {
+ return this.accountDataClientAdapter.setAccountData(type, content);
+ }
+
+ /**
+ * builds the operation containing all the parts that have been added to the builder
+ * @return {EncryptionSetupOperation}
+ */
+ buildOperation() {
+ const accountData = this.accountDataClientAdapter._values;
+ return new EncryptionSetupOperation(
+ accountData,
+ this._crossSigningKeys,
+ this._keyBackupInfo,
+ this._keySignatures,
+ );
+ }
+
+ /**
+ * Stores the created keys locally.
+ *
+ * This does not yet store the operation in a way that it can be restored,
+ * but that is the idea in the future.
+ *
+ * @param {Crypto} crypto
+ * @return {Promise}
+ */
+ async persist(crypto) {
+ // store self_signing and user_signing private key in cache
+ if (this._crossSigningKeys) {
+ const cacheCallbacks = createCryptoStoreCacheCallbacks(
+ crypto._cryptoStore, crypto._olmDevice);
+ for (const type of ["self_signing", "user_signing"]) {
+ // logger.log(`Cache ${type} cross-signing private key locally`);
+ const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
+ await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
+ }
+ // store own cross-sign pubkeys as trusted
+ await crypto._cryptoStore.doTxn(
+ 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
+ (txn) => {
+ crypto._cryptoStore.storeCrossSigningKeys(
+ txn, this._crossSigningKeys.keys);
+ },
+ );
+ }
+ // store session backup key in cache
+ if (this._sessionBackupPrivateKey) {
+ await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
+ }
+ }
+}
+
+/**
+ * Can be created from EncryptionSetupBuilder, or
+ * (in a follow-up PR, not implemented yet) restored from storage, to retry.
+ *
+ * It does not have knowledge of any private keys, unlike the builder.
+ */
+export class EncryptionSetupOperation {
+ /**
+ * @param {Map<String, Object>} accountData
+ * @param {Object} crossSigningKeys
+ * @param {Object} keyBackupInfo
+ * @param {Object} keySignatures
+ */
+ constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
+ this._accountData = accountData;
+ this._crossSigningKeys = crossSigningKeys;
+ this._keyBackupInfo = keyBackupInfo;
+ this._keySignatures = keySignatures;
+ }
+
+ /**
+ * Runs the (remaining part of, in the future) operation by sending requests to the server.
+ * @param {Crypto} crypto
+ */
+ async apply(crypto) {
+ const baseApis = crypto._baseApis;
+ // set account data
+ if (this._accountData) {
+ for (const [type, content] of this._accountData) {
+ await baseApis.setAccountData(type, content);
+ }
+ }
+ // upload cross-signing keys
+ if (this._crossSigningKeys) {
+ const keys = {};
+ for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
+ keys[name + "_key"] = key;
+ }
+ await baseApis.uploadDeviceSigningKeys(
+ this._crossSigningKeys.auth,
+ keys,
+ );
+ // pass the new keys to the main instance of our own CrossSigningInfo.
+ crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
+ }
+ // upload first cross-signing signatures with the new key
+ // (e.g. signing our own device)
+ if (this._keySignatures) {
+ await baseApis.uploadKeySignatures(this._keySignatures);
+ }
+ // need to create/update key backup info
+ if (this._keyBackupInfo) {
+ if (this._keyBackupInfo.version) {
+ // session backup signature
+ // The backup is trusted because the user provided the private key.
+ // Sign the backup with the cross signing key so the key backup can
+ // be trusted via cross-signing.
+ await baseApis._http.authedRequest(
+ undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
+ undefined, {
+ algorithm: this._keyBackupInfo.algorithm,
+ auth_data: this._keyBackupInfo.auth_data,
+ },
+ {prefix: PREFIX_UNSTABLE},
+ );
+ } else {
+ // add new key backup
+ await baseApis._http.authedRequest(
+ undefined, "POST", "/room_keys/version",
+ undefined, this._keyBackupInfo,
+ {prefix: PREFIX_UNSTABLE},
+ );
+ }
+ }
+ }
+}
+
+
+/**
+ * Catches account data set by SecretStorage during bootstrapping by
+ * implementing the methods related to account data in MatrixClient
+ */
+class AccountDataClientAdapter extends EventEmitter {
+ /**
+ * @param {Object.<String, MatrixEvent>} accountData existing account data
+ */
+ constructor(accountData) {
+ super();
+ this._existingValues = accountData;
+ this._values = new Map();
+ }
+
+ /**
+ * @param {String} type
+ * @return {Promise<Object>} the content of the account data
+ */
+ getAccountDataFromServer(type) {
+ return Promise.resolve(this.getAccountData(type));
+ }
+
+ /**
+ * @param {String} type
+ * @return {Object} the content of the account data
+ */
+ getAccountData(type) {
+ const modifiedValue = this._values.get(type);
+ if (modifiedValue) {
+ return modifiedValue;
+ }
+ const existingValue = this._existingValues[type];
+ if (existingValue) {
+ return existingValue.getContent();
+ }
+ return null;
+ }
+
+ /**
+ * @param {String} type
+ * @param {Object} content
+ * @return {Promise}
+ */
+ setAccountData(type, content) {
+ this._values.set(type, content);
+ // ensure accountData is emitted on the next tick,
+ // as SecretStorage listens for it while calling this method
+ // and it seems to rely on this.
+ return Promise.resolve().then(() => {
+ const event = new MatrixEvent({type, content});
+ this.emit("accountData", event);
+ });
+ }
+}
+
+/**
+ * Catches the private cross-signing keys set during bootstrapping
+ * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
+ * See CrossSigningInfo constructor
+ */
+class CrossSigningCallbacks {
+ constructor() {
+ this.privateKeys = new Map();
+ }
+
+ // cache callbacks
+ getCrossSigningKeyCache(type, expectedPublicKey) {
+ return this.getCrossSigningKey(type, expectedPublicKey);
+ }
+
+ storeCrossSigningKeyCache(type, key) {
+ this.privateKeys.set(type, key);
+ return Promise.resolve();
+ }
+
+ // non-cache callbacks
+ getCrossSigningKey(type, _expectedPubkey) {
+ return Promise.resolve(this.privateKeys.get(type));
+ }
+
+ saveCrossSigningKeys(privateKeys) {
+ for (const [type, privateKey] of Object.entries(privateKeys)) {
+ this.privateKeys.set(type, privateKey);
+ }
+ }
+}
+
+/**
+ * Catches the 4S private key set during bootstrapping by implementing
+ * the SecretStorage crypto callbacks
+ */
+class SSSSCryptoCallbacks {
+ constructor() {
+ this._privateKeys = new Map();
+ }
+
+ getSecretStorageKey({ keys }, name) {
+ for (const keyId of Object.keys(keys)) {
+ const privateKey = this._privateKeys.get(keyId);
+ if (privateKey) {
+ return [keyId, privateKey];
+ }
+ }
+ }
+
+ addPrivateKey(keyId, privKey) {
+ this._privateKeys.set(keyId, privKey);
+ }
+}
diff --git a/src/crypto/index.js b/src/crypto/index.js
index b88b5a16..f2265374 100644
--- a/src/crypto/index.js
+++ b/src/crypto/index.js
@@ -34,11 +34,11 @@ import {DeviceInfo} from "./deviceinfo";
import * as algorithms from "./algorithms";
import {
CrossSigningInfo,
- CrossSigningLevel,
DeviceTrustLevel,
UserTrustLevel,
createCryptoStoreCacheCallbacks,
} from './CrossSigning';
+import {EncryptionSetupBuilder} from "./EncryptionSetup";
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
@@ -49,11 +49,10 @@ import {
} from './verification/QRCode';
import {SAS} from './verification/SAS';
import {keyFromPassphrase} from './key_passphrase';
-import {encodeRecoveryKey} from './recoverykey';
+import {encodeRecoveryKey, decodeRecoveryKey} from './recoverykey';
import {VerificationRequest} from "./verification/request/VerificationRequest";
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
-import * as httpApi from "../http-api";
import {IllegalMethod} from "./verification/IllegalMethod";
import {KeySignatureUploadError} from "../errors";
import {decryptAES, encryptAES} from './aes';
@@ -450,11 +449,11 @@ Crypto.prototype.isCrossSigningReady = async function() {
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
- * @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
+ * @param {function} opts.authUploadDeviceSigningKeys Function
* called to await an interactive auth flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives the
- * auth data as an object.
+ * auth data as an object. Can be called multiple times, first with an empty authDict, to obtain the flows.
* @param {function} [opts.createSecretStorageKey] Optional. Function
* called to await a secret storage key creation flow.
* Returns:
@@ -474,6 +473,7 @@ Crypto.prototype.isCrossSigningReady = async function() {
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
*/
+
Crypto.prototype.bootstrapSecretStorage = async function({
authUploadDeviceSigningKeys,
createSecretStorageKey = async () => ({ }),
@@ -483,42 +483,18 @@ Crypto.prototype.bootstrapSecretStorage = async function({
getKeyBackupPassphrase,
} = {}) {
logger.log("Bootstrapping Secure Secret Storage");
-
- // Create cross-signing keys if they don't exist, as we want to sign the SSSS default
- // key with the cross-signing master key. The cross-signing master key is also used
- // to verify the signature on the SSSS default key when adding secrets, so we
- // effectively need it for both reading and writing secrets.
- const crossSigningPrivateKeys = {};
-
- // If we happen to reset cross-signing keys here, then we want access to the
- // cross-signing private keys, but only for the scope of this method, so we
- // use temporary callbacks to weave them through the various APIs.
- const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
+ const builder = new EncryptionSetupBuilder(this._baseApis.store.accountData);
+ const secretStorage = new SecretStorage(
+ builder.accountDataClientAdapter,
+ builder.ssssCryptoCallbacks);
+ const crossSigningInfo = new CrossSigningInfo(
+ this._userId,
+ builder.crossSigningCallbacks,
+ builder.crossSigningCallbacks);
// 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;
- };
-
// create a new SSSS key and set it as default
const createSSSS = async (opts, privateKey) => {
opts = opts || {};
@@ -526,28 +502,46 @@ Crypto.prototype.bootstrapSecretStorage = async function({
opts.key = privateKey;
}
- const keyId = await this.addSecretStorageKey(
+ const keyId = await secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
- await this.setDefaultSecretStorageKeyId(keyId);
if (privateKey) {
- // cache the private key so that we can access it again
- ssssKeys[keyId] = privateKey;
+ // make the private key available to encrypt 4S secrets
+ builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
}
+
+ await secretStorage.setDefaultKeyId(keyId);
return keyId;
};
// reset the cross-signing keys
const resetCrossSigning = async () => {
- this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
- keys => Object.assign(crossSigningPrivateKeys, keys);
- this._baseApis._cryptoCallbacks.getCrossSigningKey =
- name => crossSigningPrivateKeys[name];
- await this.resetCrossSigningKeys(
- CrossSigningLevel.MASTER,
- { authUploadDeviceSigningKeys },
- );
+ crossSigningInfo.resetKeys();
+ // sign master key with device key
+ await this._signObject(crossSigningInfo.keys.master);
+
+ await authUploadDeviceSigningKeys(authDict => {
+ if (authDict) {
+ builder.addCrossSigningKeys(authDict, crossSigningInfo.keys);
+ return Promise.resolve();
+ } else {
+ // This callback also gets called to obtain the IUA flows,
+ // so do a call to obtain those if we don't have the authDict yet
+ // We should get called again at a later point with the authDict.
+ return this._baseApis.uploadDeviceSigningKeys(null, {});
+ }
+ });
+
+ // cross-sign own device
+ const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
+ const deviceSignature = await crossSigningInfo.signDevice(this._userId, device);
+ builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
+
+ if (keyBackupInfo) {
+ await crossSigningInfo.signObject(keyBackupInfo.auth_data, "master");
+ builder.addSessionBackup(keyBackupInfo);
+ }
};
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
@@ -557,186 +551,185 @@ Crypto.prototype.bootstrapSecretStorage = async function({
);
if (key) {
const keyData = key[1];
- ssssKeys[keyId] = keyData;
+ builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
keyInfo.iv = iv;
keyInfo.mac = mac;
- await this._baseApis.setAccountData(
+ await builder.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
);
}
}
};
- try {
- const oldSSSSKey = await this.getSecretStorageKey();
- const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
- const decryptionKeys =
- await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
- const inStorage = !setupNewSecretStorage && decryptionKeys;
-
- if (!inStorage && !keyBackupInfo) {
- // either we don't have anything, or we've been asked to restart
- // from scratch
- logger.log(
- "Cross-signing private keys not found in secret storage, " +
- "creating new keys",
- );
-
- await resetCrossSigning();
-
- if (
- setupNewSecretStorage ||
- !oldKeyInfo ||
- oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
- ) {
- // if we already have a usable default SSSS key and aren't resetting SSSS just use it.
- // otherwise, create a new one
- // Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
- // We could move them to the new key but a) that would mean we'd need to prompt for the old
- // passphrase, and b) it's not clear that would be the right thing to do anyway.
- const { keyInfo, privateKey } = await createSecretStorageKey();
- newKeyId = await createSSSS(keyInfo, privateKey);
- }
+ const oldSSSSKey = await this.getSecretStorageKey();
+ const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
+ const decryptionKeys =
+ await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
+ const inStorage = !setupNewSecretStorage && decryptionKeys;
- if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
- }
- } else if (!inStorage && keyBackupInfo) {
- // we have an existing backup, but no SSSS
-
- logger.log("Secret storage default key not found, using key backup key");
-
- // if we have the backup key already cached, use it; otherwise use the
- // callback to prompt for the key
- const backupKey = await this.getSessionBackupPrivateKey() ||
- await getKeyBackupPassphrase();
+ if (!inStorage && !keyBackupInfo) {
+ // either we don't have anything, or we've been asked to restart
+ // from scratch
+ logger.log(
+ "Cross-signing private keys not found in secret storage, " +
+ "creating new keys",
+ );
- // create new cross-signing keys
- await resetCrossSigning();
+ await resetCrossSigning();
+
+ if (
+ setupNewSecretStorage ||
+ !oldKeyInfo ||
+ oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
+ ) {
+ // if we already have a usable default SSSS key and aren't resetting SSSS just use it.
+ // otherwise, create a new one
+ // Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
+ // We could move them to the new key but a) that would mean we'd need to prompt for the old
+ // passphrase, and b) it's not clear that would be the right thing to do anyway.
+ const { keyInfo, privateKey } = await createSecretStorageKey();
+ newKeyId = await createSSSS(keyInfo, privateKey);
+ }
+ } else if (!inStorage && keyBackupInfo) {
+ // we have an existing backup, but no SSSS
- // create a new SSSS key and use the backup key as the new SSSS key
- const opts = {};
+ logger.log("Secret storage default key not found, using key backup 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,
- bits: 256,
- };
- }
+ // if we have the backup key already cached, use it; otherwise use the
+ // callback to prompt for the key
+ const backupKey = await this.getSessionBackupPrivateKey() ||
+ await getKeyBackupPassphrase();
- newKeyId = await createSSSS(opts, backupKey);
+ // create new cross-signing keys
+ await resetCrossSigning();
- // store the backup key in secret storage
- await this.storeSecret(
- "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
- );
+ // create a new SSSS key and use the backup key as the new SSSS key
+ const opts = {};
- // The backup is trusted because the user provided the private key.
- // Sign the backup with the cross signing key so the key backup can
- // be trusted via cross-signing.
- logger.log("Adding cross signing signature to key backup");
- await this._crossSigningInfo.signObject(
- keyBackupInfo.auth_data, "master",
- );
- await this._baseApis._http.authedRequest(
- undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
- undefined, keyBackupInfo,
- {prefix: httpApi.PREFIX_UNSTABLE},
- );
- } else if (!this._crossSigningInfo.getId()) {
- // we have SSSS, but we don't know if the server's cross-signing
- // keys should be trusted
- logger.log("Cross-signing private keys found in secret storage");
+ 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,
+ };
+ }
- // fetch the private keys and set up our local copy of the keys for
- // use
- await this.checkOwnCrossSigningTrust();
+ newKeyId = await createSSSS(opts, backupKey);
- if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- // make sure that the default key has the information needed to
- // check the passphrase
- await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
- }
- } else {
- // we have SSSS and we cross-signing is already set up
- logger.log("Cross signing keys are present in secret storage");
+ // store the backup key in secret storage
+ await secretStorage.store(
+ "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
+ );
- if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- // make sure that the default key has the information needed to
- // check the passphrase
- await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
- }
+ // The backup is trusted because the user provided the private key.
+ // Sign the backup with the cross signing key so the key backup can
+ // be trusted via cross-signing.
+ logger.log("Adding cross signing signature to key backup");
+ await crossSigningInfo.signObject(
+ keyBackupInfo.auth_data, "master",
+ );
+ builder.addSessionBackup(keyBackupInfo);
+ } else if (!this._crossSigningInfo.getId()) {
+ // we have SSSS, but we don't know if the server's cross-signing
+ // keys should be trusted
+ logger.log("Cross-signing private keys found in secret storage");
+
+ // TODO: take this use case out of bootstrapping
+ // fetch the private keys and set up our local copy of the keys for
+ // use
+ //
+ // so if some other device resets the cross-signing keys,
+ // we mark them as untrusted from _onDeviceListUserCrossSigningUpdated
+ // you can either fix this by hitting the verify this session which (might?) call this method,
+ // or the reset button in the settings
+ await this.checkOwnCrossSigningTrust();
+
+ if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
+ // make sure that the default key has the information needed to
+ // check the passphrase
+ await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
+ } else {
+ // we have SSSS and we cross-signing is already set up
+ logger.log("Cross signing keys are present in secret storage");
- // If cross-signing keys were reset, store them in Secure Secret Storage.
- // This is done in a separate step so we can ensure secret storage has its
- // own key first.
- // XXX: We need to think about how to re-do these steps if they fail.
- // See also https://github.com/vector-im/riot-web/issues/11635
- if (Object.keys(crossSigningPrivateKeys).length) {
- logger.log("Storing cross-signing private keys in secret storage");
- // Assuming no app-supplied callback, default to storing in SSSS.
- if (!appCallbacks.saveCrossSigningKeys) {
- await CrossSigningInfo.storeInSecretStorage(
- crossSigningPrivateKeys,
- this._secretStorage,
- );
- }
+ if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
+ // make sure that the default key has the information needed to
+ // check the passphrase
+ await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
+ }
- if (setupNewKeyBackup && !keyBackupInfo) {
- const info = await this._baseApis.prepareKeyBackupVersion(
- null /* random key */,
- { secureSecretStorage: true },
+ const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
+ if (crossSigningPrivateKeys.size) {
+ logger.log("Storing cross-signing private keys in secret storage");
+ // Assuming no app-supplied callback, default to storing in SSSS.
+ if (!this._baseApis._cryptoCallbacks.saveCrossSigningKeys) {
+ // this is writing to in-memory account data in builder.accountDataClientAdapter
+ // so won't fail
+ await CrossSigningInfo.storeInSecretStorage(
+ crossSigningPrivateKeys,
+ secretStorage,
);
- await this._baseApis.createKeyBackupVersion(info);
}
+ }
- // Call `getCrossSigningKey` for side effect of caching private keys for
- // future gossiping to other devices if enabled via app level callbacks.
- if (this._crossSigningInfo._cacheCallbacks) {
- for (const type of ["self_signing", "user_signing"]) {
- logger.log(`Cache ${type} cross-signing private key locally`);
- await this._crossSigningInfo.getCrossSigningKey(type);
- }
- }
+ if (setupNewKeyBackup && !keyBackupInfo) {
+ const info = await this._baseApis.prepareKeyBackupVersion(
+ null /* random key */,
+ // don't write to secret storage, as it will write to this._secretStorage.
+ // Here, we want to capture all the side-effects of bootstrapping,
+ // and want to write to the local secretStorage object
+ { secureSecretStorage: false },
+ );
+ // write the key ourselves to 4S
+ const privateKey = decodeRecoveryKey(info.recovery_key);
+ await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
+
+ // create keyBackupInfo object to add to builder
+ const data = {
+ algorithm: info.algorithm,
+ auth_data: info.auth_data,
+ };
+ // sign with cross-sign master key
+ await crossSigningInfo.signObject(data.auth_data, "master");
+ // sign with the device fingerprint
+ await this._signObject(data.auth_data);
- // and likewise for the session backup key
- const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
- if (sessionBackupKey) {
- logger.info("Got session backup key from secret storage: caching");
- // fix up the backup key if it's in the wrong format, and replace
- // in secret storage
- const fixedBackupKey = fixBackupKey(sessionBackupKey);
- if (fixedBackupKey) {
- await this.storeSecret(
- "m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId],
- );
- }
- const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
- fixedBackupKey || sessionBackupKey,
- ));
- await this.storeSessionBackupPrivateKey(decodedBackupKey);
- }
- } finally {
- // Restore the original callbacks. NB. we must do this by manipulating
- // the same object since the CrossSigning class has a reference to the
- // object, so if we assign the object here then our callbacks will change
- // but the instances of the CrossSigning class will be left with our
- // random, otherwise dead closures.
- for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
- delete this._baseApis._cryptoCallbacks[cb];
+
+ builder.addSessionBackup(data);
+ }
+
+ // and likewise for the session backup key
+ const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1');
+ if (sessionBackupKey) {
+ logger.info("Got session backup key from secret storage: caching");
+ // fix up the backup key if it's in the wrong format, and replace
+ // in secret storage
+ const fixedBackupKey = fixBackupKey(sessionBackupKey);
+ if (fixedBackupKey) {
+ await secretStorage.store("m.megolm_backup.v1",
+ fixedBackupKey, [newKeyId || oldKeyId],
+ );
}
- Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
+ const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
+ fixedBackupKey || sessionBackupKey,
+ ));
+ await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
}
+ const operation = builder.buildOperation();
+ await operation.apply(this);
+ // this persists private keys and public keys as trusted,
+ // only do this if apply succeeded for now as retry isn't in place yet
+ await builder.persist(this);
+
logger.log("Secure Secret Storage ready");
};
@@ -898,57 +891,6 @@ Crypto.prototype.checkCrossSigningPrivateKey = function(privateKey, expectedPubl
};
/**
- * Generate new cross-signing keys.
- *
- * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
- * keys will be created for the given level and below. Defaults to
- * regenerating all keys.
- * @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
- * called to await an interactive auth flow when uploading device signing keys.
- * Args:
- * {function} A function that makes the request requiring auth. Receives the
- * auth data as an object.
- */
-Crypto.prototype.resetCrossSigningKeys = async function(level, {
- authUploadDeviceSigningKeys = async func => await func(),
-} = {}) {
- logger.info(`Resetting cross-signing keys at level ${level}`);
- // Copy old keys (usually empty) in case we need to revert
- const oldKeys = Object.assign({}, this._crossSigningInfo.keys);
- try {
- await this._crossSigningInfo.resetKeys(level);
- await this._signObject(this._crossSigningInfo.keys.master);
-
- // send keys to server first before storing as trusted locally
- // to ensure upload succeeds
- const keys = {};
- for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) {
- keys[name + "_key"] = key;
- }
- await authUploadDeviceSigningKeys(async authDict => {
- await this._baseApis.uploadDeviceSigningKeys(authDict, keys);
- });
-
- // write a copy locally so we know these are trusted keys
- await this._cryptoStore.doTxn(
- 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
- (txn) => {
- this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
- },
- );
- } catch (e) {
- // If anything failed here, revert the keys so we know to try again from the start
- // next time.
- logger.error("Resetting cross-signing keys failed, revert to previous keys", e);
- this._crossSigningInfo.keys = oldKeys;
- throw e;
- }
- this._baseApis.emit("crossSigning.keysChanged", {});
- await this._afterCrossSigningLocalKeyChange();
- logger.info("Cross-signing key reset complete");
-};
-
-/**
* Run various follow-up actions after cross-signing keys have changed locally
* (either by resetting the keys for the account or by getting them from secret
* storage), such as signing the current device, upgrading device