Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de339d3098 | |||
| 0f83234be9 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 7eeb60c838 | |||
| d42cdbbc5b | |||
| 66237e1ea6 | |||
| a51c0450c3 | |||
| 7a2416bb6d | |||
| a1528e9e33 | |||
| 71cc4d535e | |||
| c06723df3d | |||
| 06b285c013 | |||
| 49c06ef0ca | |||
| 5f92357fec | |||
| 3e0dd3d918 | |||
| f19d76b08d | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb |
+1
-1
@@ -30,7 +30,7 @@ module.exports = {
|
||||
"no-async-promise-executor": "off",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.{ts, tsx}"],
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// While we're converting to ts we make heavy use of this
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
Changes in [8.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0-rc.1) (2020-09-09)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0...v8.3.0-rc.1)
|
||||
|
||||
* Add missing options in ICreateClientOpts
|
||||
[\#1452](https://github.com/matrix-org/matrix-js-sdk/pull/1452)
|
||||
* Ensure ready functions return boolean values
|
||||
[\#1457](https://github.com/matrix-org/matrix-js-sdk/pull/1457)
|
||||
* Handle missing cross-signing keys gracefully
|
||||
[\#1456](https://github.com/matrix-org/matrix-js-sdk/pull/1456)
|
||||
* Fix eslint ts override tsx matching
|
||||
[\#1451](https://github.com/matrix-org/matrix-js-sdk/pull/1451)
|
||||
* Untangle cross-signing and secret storage
|
||||
[\#1450](https://github.com/matrix-org/matrix-js-sdk/pull/1450)
|
||||
|
||||
Changes in [8.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0) (2020-09-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0-rc.1...v8.2.0)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "8.2.0",
|
||||
"version": "8.3.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
|
||||
@@ -20,7 +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";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
@@ -71,8 +71,7 @@ describe("Cross Signing", function() {
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
@@ -118,8 +117,7 @@ describe("Cross Signing", function() {
|
||||
// through failure, stopping before actually applying changes.
|
||||
let bootstrapDidThrow = false;
|
||||
try {
|
||||
await alice.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -326,9 +326,11 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
@@ -379,13 +381,15 @@ describe("Secrets", function() {
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
}),
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
@@ -394,7 +398,7 @@ describe("Secrets", function() {
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage({
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
@@ -517,9 +521,7 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
@@ -659,9 +661,7 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
|
||||
+112
-10
@@ -235,6 +235,14 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* }
|
||||
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
|
||||
* Optional. Function called when a new encryption key for secret storage
|
||||
* has been created. This allows the application a chance to cache this key if
|
||||
* desired to avoid user prompts.
|
||||
* Args:
|
||||
* {string} keyId the ID of the new key
|
||||
* {Uint8Array} key the new private key
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
|
||||
* Optional. Function called when a request for a secret is received from another
|
||||
* device.
|
||||
@@ -1094,6 +1102,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Get the user's cross-signing key ID.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getCrossSigningId
|
||||
@@ -1105,6 +1114,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getStoredCrossSigningForUser
|
||||
@@ -1115,6 +1125,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Check whether a given user is trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkUserTrust
|
||||
@@ -1125,6 +1136,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkDeviceTrust
|
||||
@@ -1137,6 +1149,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
/**
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkOwnCrossSigningTrust
|
||||
@@ -1146,6 +1159,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
* Checks that a given cross-signing private key matches a given public key.
|
||||
* This can be used by the getCrossSigningKey callback to verify that the
|
||||
* private key it is about to supply is the one that was requested.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkCrossSigningPrivateKey
|
||||
@@ -1158,8 +1172,48 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @function module:client~MatrixClient#prepareToEncrypt
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account and trusted by this device
|
||||
* - has private keys either cached locally or stored in secret storage
|
||||
*
|
||||
* If this function returns false, bootstrapCrossSigning() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isCrossSigningReady
|
||||
* @return {bool} True if cross-signing is ready to be used on this device
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap cross-signing by creating keys if needed. If everything is already
|
||||
* set up, then no changes are made, so this is safe to run to ensure
|
||||
* cross-signing is ready for use.
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been setup)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#bootstrapCrossSigning
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
|
||||
* already exist.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getCrossSigningId",
|
||||
"getStoredCrossSigningForUser",
|
||||
@@ -1170,6 +1224,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
"legacyDeviceVerification",
|
||||
"prepareToEncrypt",
|
||||
"isCrossSigningReady",
|
||||
"bootstrapCrossSigning",
|
||||
"getCryptoTrustCrossSignedDevices",
|
||||
"setCryptoTrustCrossSignedDevices",
|
||||
"countSessionsNeedingBackup",
|
||||
@@ -1196,6 +1251,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Create a recovery key from a user-supplied passphrase.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#createRecoveryKeyFromPassphrase
|
||||
@@ -1208,22 +1264,59 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key and signing it with
|
||||
* the cross-signing master key. If everything is already set up, then no
|
||||
* changes are made, so this is safe to run to ensure secret storage is ready
|
||||
* for use.
|
||||
* Checks whether secret storage:
|
||||
* - is enabled on this account
|
||||
* - is storing cross-signing private keys
|
||||
* - is storing session backup key (if enabled)
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isSecretStorageReady
|
||||
* @return {bool} True if secret storage is ready to be used on this device
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
|
||||
* already set up, then no changes are made, so this is safe to run to ensure secret
|
||||
* storage is ready for use.
|
||||
*
|
||||
* This function
|
||||
* - creates a new Secure Secret Storage key if no default key exists
|
||||
* - if a key backup exists, it is migrated to store the key in the Secret
|
||||
* Storage
|
||||
* - creates a backup if none exists, and one is requested
|
||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||
* algorithm is found
|
||||
*
|
||||
* @function module:client~MatrixClient#bootstrapSecretStorage
|
||||
* @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.
|
||||
* @param {function} [opts.createSecretStorageKey] Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
* {Promise<Object>} Object with public key metadata, encoded private
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
* @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a key for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#addSecretStorageKey
|
||||
@@ -1238,6 +1331,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Check whether we have a key with a given ID.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#hasSecretStorageKey
|
||||
@@ -1247,7 +1341,8 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
*/
|
||||
|
||||
/**
|
||||
* Store an encrypted secret on the server
|
||||
* Store an encrypted secret on the server.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#storeSecret
|
||||
@@ -1259,6 +1354,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Get a secret from storage.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getSecret
|
||||
@@ -1269,6 +1365,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Check if a secret is stored on the server.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isSecretStored
|
||||
@@ -1283,6 +1380,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Request a secret from another device.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#requestSecret
|
||||
@@ -1294,6 +1392,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Get the current default key ID for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getDefaultSecretStorageKeyId
|
||||
@@ -1303,6 +1402,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Set the current default key ID for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#setDefaultSecretStorageKeyId
|
||||
@@ -1313,6 +1413,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
* Checks that a given secret storage private key matches a given public key.
|
||||
* This can be used by the getSecretStorageKey callback to verify that the
|
||||
* private key it is about to supply is the one that was requested.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkSecretStoragePrivateKey
|
||||
@@ -1324,6 +1425,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getEventEncryptionInfo",
|
||||
"createRecoveryKeyFromPassphrase",
|
||||
"isSecretStorageReady",
|
||||
"bootstrapSecretStorage",
|
||||
"addSecretStorageKey",
|
||||
"hasSecretStorageKey",
|
||||
|
||||
+54
-18
@@ -66,8 +66,27 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
this.crossSigningVerifiedBefore = false;
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the app callback to ask for a private key
|
||||
*
|
||||
* @param {string} type The key type ("master", "self_signing", or "user_signing")
|
||||
* @param {string} expectedPubkey The matching public key or undefined to use
|
||||
* the stored public key for the given key type.
|
||||
@@ -127,24 +146,6 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in secret storage.
|
||||
* XXX: This could be static, be we often seem to have an instance when we
|
||||
@@ -201,9 +202,44 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*/
|
||||
static async getFromSecretStorage(type, secretStorage) {
|
||||
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
|
||||
if (!encodedKey) {
|
||||
return null;
|
||||
}
|
||||
return decodeBase64(encodedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in the local key cache.
|
||||
*
|
||||
* @returns {boolean} True if all keys are stored in the local cache.
|
||||
*/
|
||||
async isStoredInKeyCache() {
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return false;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache(type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cross-signing private keys from the local cache.
|
||||
*
|
||||
* @returns {Map} A map from key type (string) to private key (Uint8Array)
|
||||
*/
|
||||
async getCrossSigningKeysFromCache() {
|
||||
const keys = new Map();
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return keys;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
|
||||
keys.set(type, privKey);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID used to identify the user. This can also be used to test for
|
||||
* the existence of a given key type.
|
||||
|
||||
@@ -346,5 +346,12 @@ class SSSSCryptoCallbacks {
|
||||
|
||||
addPrivateKey(keyId, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
if (
|
||||
this._delegateCryptoCallbacks &&
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
||||
) {
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+225
-119
@@ -406,14 +406,12 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account
|
||||
* - is trusted by this device
|
||||
* - has private keys stored in secret storage
|
||||
* and that the account has a secret storage key
|
||||
* - is enabled on this account and trusted by this device
|
||||
* - has private keys either cached locally or stored in secret storage
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* If this function returns false, bootstrapCrossSigning() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
@@ -422,24 +420,179 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
*/
|
||||
Crypto.prototype.isCrossSigningReady = async function() {
|
||||
const publicKeysOnDevice = this._crossSigningInfo.getId();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
const privateKeysExistSomewhere = (
|
||||
await this._crossSigningInfo.isStoredInKeyCache() ||
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
)
|
||||
);
|
||||
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
|
||||
|
||||
return (
|
||||
return !!(
|
||||
publicKeysOnDevice &&
|
||||
privateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
privateKeysExistSomewhere
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether secret storage:
|
||||
* - is enabled on this account
|
||||
* - is storing cross-signing private keys
|
||||
* - is storing session backup key (if enabled)
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @return {bool} True if secret storage is ready to be used on this device
|
||||
*/
|
||||
Crypto.prototype.isSecretStorageReady = async function() {
|
||||
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
);
|
||||
const sessionBackupInStorage = (
|
||||
!this._baseApis.getKeyBackupEnabled() ||
|
||||
this._baseApis.isKeyBackupKeyStored()
|
||||
);
|
||||
|
||||
return !!(
|
||||
secretStorageKeyInAccount &&
|
||||
privateKeysInStorage &&
|
||||
sessionBackupInStorage
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key and
|
||||
* signing it with the cross-signing master key. If everything is already set
|
||||
* up, then no changes are made, so this is safe to run to ensure secret storage
|
||||
* is ready for use.
|
||||
* Bootstrap cross-signing by creating keys if needed. If everything is already
|
||||
* set up, then no changes are made, so this is safe to run to ensure
|
||||
* cross-signing is ready for use.
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been setup)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
|
||||
* already exist.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
Crypto.prototype.bootstrapCrossSigning = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
setupNewCrossSigning,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping cross-signing");
|
||||
|
||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
this._baseApis.store.accountData,
|
||||
delegateCryptoCallbacks,
|
||||
);
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
this._userId,
|
||||
builder.crossSigningCallbacks,
|
||||
builder.crossSigningCallbacks,
|
||||
);
|
||||
|
||||
// Reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
crossSigningInfo.resetKeys();
|
||||
// Sign master key with device key
|
||||
await this._signObject(crossSigningInfo.keys.master);
|
||||
|
||||
// Store auth flow helper function, as we need to call it when uploading
|
||||
// to ensure we handle auth errors properly.
|
||||
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
|
||||
|
||||
// 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);
|
||||
|
||||
// Sign message key backup with cross-signing master key
|
||||
if (this.backupInfo) {
|
||||
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
|
||||
builder.addSessionBackup(this.backupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const publicKeysOnDevice = this._crossSigningInfo.getId();
|
||||
const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
);
|
||||
const privateKeysExistSomewhere = (
|
||||
privateKeysInCache ||
|
||||
privateKeysInStorage
|
||||
);
|
||||
|
||||
if (!privateKeysExistSomewhere || setupNewCrossSigning) {
|
||||
logger.log(
|
||||
"Cross-signing private keys not found locally or in secret storage, " +
|
||||
"creating new keys",
|
||||
);
|
||||
// If a user has multiple devices, it important to only call bootstrap
|
||||
// as part of some UI flow (and not silently during startup), as they
|
||||
// may have setup cross-signing on a platform which has not saved keys
|
||||
// to secret storage, and this would reset them. In such a case, you
|
||||
// should prompt the user to verify any existing devices first (and
|
||||
// request private keys from those devices) before calling bootstrap.
|
||||
await resetCrossSigning();
|
||||
} else if (publicKeysOnDevice && privateKeysInCache) {
|
||||
logger.log(
|
||||
"Cross-signing public keys trusted and private keys found locally",
|
||||
);
|
||||
} else if (privateKeysInStorage) {
|
||||
logger.log(
|
||||
"Cross-signing private keys not found locally, but they are available " +
|
||||
"in secret storage, reading storage and caching locally",
|
||||
);
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
}
|
||||
|
||||
// Assuming no app-supplied callback, default to storing new private keys in
|
||||
// secret storage if it exists. If it does not, it is assumed this will be
|
||||
// done as part of setting up secret storage later.
|
||||
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
||||
if (
|
||||
crossSigningPrivateKeys.size &&
|
||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys
|
||||
) {
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks);
|
||||
if (await secretStorage.hasKey()) {
|
||||
logger.log("Storing new cross-signing private keys in secret storage");
|
||||
// This is writing to in-memory account data in
|
||||
// builder.accountDataClientAdapter so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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("Cross-signing ready");
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
|
||||
* already set up, then no changes are made, so this is safe to run to ensure secret
|
||||
* storage is ready for use.
|
||||
*
|
||||
* This function
|
||||
* - creates a new Secure Secret Storage key if no default key exists
|
||||
@@ -449,12 +602,8 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||
* algorithm is found
|
||||
*
|
||||
* @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. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {function} [opts.createSecretStorageKey] Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
@@ -474,9 +623,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 () => ({ }),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
@@ -491,11 +638,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks);
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
this._userId,
|
||||
builder.crossSigningCallbacks,
|
||||
builder.crossSigningCallbacks);
|
||||
builder.ssssCryptoCallbacks,
|
||||
);
|
||||
|
||||
// the ID of the new SSSS key, if we create one
|
||||
let newKeyId = null;
|
||||
@@ -520,27 +664,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
return keyId;
|
||||
};
|
||||
|
||||
// reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
crossSigningInfo.resetKeys();
|
||||
// sign master key with device key
|
||||
await this._signObject(crossSigningInfo.keys.master);
|
||||
|
||||
// Store auth flow helper function, as we need to call it when uploading
|
||||
// to ensure we handle auth errors properly.
|
||||
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
|
||||
|
||||
// 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) => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey(
|
||||
@@ -562,46 +685,36 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
const storageExists = (
|
||||
!setupNewSecretStorage &&
|
||||
oldKeyInfo &&
|
||||
oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
);
|
||||
|
||||
if (!inStorage && !keyBackupInfo) {
|
||||
if (!storageExists && !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",
|
||||
"Secret storage does not exist, creating new storage key",
|
||||
);
|
||||
|
||||
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) {
|
||||
// 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 (!storageExists && keyBackupInfo) {
|
||||
// we have an existing backup, but no SSSS
|
||||
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
logger.log("Secret storage does not exist, 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();
|
||||
|
||||
// create new cross-signing keys
|
||||
await resetCrossSigning();
|
||||
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts = {};
|
||||
|
||||
@@ -625,36 +738,16 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// 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(
|
||||
await this._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");
|
||||
// 4S is already set up
|
||||
logger.log("Secret storage exists");
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
@@ -663,21 +756,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
// If we have cross-signing private keys cached, store them in secret
|
||||
// storage if they are not there already.
|
||||
if (
|
||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys &&
|
||||
await this.isCrossSigningReady() &&
|
||||
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
|
||||
) {
|
||||
logger.log("Copying cross-signing private keys from cache to secret storage");
|
||||
const crossSigningPrivateKeys =
|
||||
await this._crossSigningInfo.getCrossSigningKeysFromCache();
|
||||
// This is writing to in-memory account data in
|
||||
// builder.accountDataClientAdapter so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
logger.log("Creating new message key backup version");
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
// don't write to secret storage, as it will write to this._secretStorage.
|
||||
@@ -695,15 +793,14 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
// sign with cross-sign master key
|
||||
await crossSigningInfo.signObject(data.auth_data, "master");
|
||||
await this._crossSigningInfo.signObject(data.auth_data, "master");
|
||||
// sign with the device fingerprint
|
||||
await this._signObject(data.auth_data);
|
||||
|
||||
|
||||
builder.addSessionBackup(data);
|
||||
}
|
||||
|
||||
// and likewise for the session backup key
|
||||
// Cache 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");
|
||||
@@ -2025,9 +2122,18 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
// do cross-signing
|
||||
if (verified && userId === this._userId) {
|
||||
logger.info("Own device " + deviceId + " marked verified: signing");
|
||||
const device = await this._crossSigningInfo.signDevice(
|
||||
userId, DeviceInfo.fromStorage(dev, deviceId),
|
||||
);
|
||||
|
||||
// Signing only needed if other device not already signed
|
||||
let device;
|
||||
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
|
||||
if (deviceTrust.isCrossSigningVerified()) {
|
||||
logger.log(`Own device ${deviceId} already cross-signing verified`);
|
||||
} else {
|
||||
device = await this._crossSigningInfo.signDevice(
|
||||
userId, DeviceInfo.fromStorage(dev, deviceId),
|
||||
);
|
||||
}
|
||||
|
||||
if (device) {
|
||||
const upload = async ({shouldEmit}) => {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
|
||||
@@ -120,6 +120,12 @@ interface ICreateClientOpts {
|
||||
cryptoStore?: CryptoStore;
|
||||
scheduler?: MatrixScheduler;
|
||||
request?: Request;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
identityServer?: any;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
queryParams?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user