Compare commits

..

25 Commits

Author SHA1 Message Date
RiotRobot de339d3098 v8.3.0-rc.1 2020-09-09 15:06:48 +01:00
RiotRobot 0f83234be9 Prepare changelog for v8.3.0-rc.1 2020-09-09 15:06:47 +01:00
J. Ryan Stinnett eec7c4c61b Merge pull request #1452 from dalcde/develop
Add missing options in ICreateClientOpts
2020-09-03 15:51:25 +01:00
J. Ryan Stinnett 25b4b049b7 Merge pull request #1457 from matrix-org/jryans/is-x-ready-bools
Ensure ready functions return boolean values
2020-09-02 17:30:16 +01:00
J. Ryan Stinnett 2191eb3f41 Ensure ready functions return boolean values
Fixes https://github.com/vector-im/element-web/issues/15087
2020-09-02 17:18:47 +01:00
J. Ryan Stinnett bebdbf7e05 Merge pull request #1456 from matrix-org/jryans/cross-signing-verif-errors
Handle missing cross-signing keys gracefully
2020-09-02 15:07:27 +01:00
J. Ryan Stinnett 646c091966 Skip self-signing if device already signed
When adding a device to your account, both devices attempt to sign each other
using the self-signing key, but in reality only the new device needs to be
signed. This avoids a case where web would fail verification while attempting to
sign the old device because of missing private keys (since they aren't in 4S and
it hasn't requested them from the other device yet), even though signing the old
device is redundant anyway.

Part of https://github.com/vector-im/element-web/issues/14970
2020-09-02 12:54:26 +01:00
J. Ryan Stinnett 952729cb1b Abort early if cross-signing key not found
This helps us print a better error message when the key does not exist.

Part of https://github.com/vector-im/element-web/issues/14970
2020-09-02 12:32:35 +01:00
RiotRobot c6992e2056 Merge branch 'master' into develop 2020-09-01 17:00:47 +01:00
Michael Telatynski 7eeb60c838 Merge pull request #1451 from matrix-org/t3chguy/lint-ts
Fix eslint ts override tsx matching
2020-09-01 09:10:22 +01:00
Dexter Chua d42cdbbc5b Add missing options in ICreateClientOpts
This makes the examples in the README actually compile.

Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-08-29 14:40:40 +08:00
Michael Telatynski 66237e1ea6 Fix eslint ts override tsx matching 2020-08-29 01:10:08 +01:00
J. Ryan Stinnett a51c0450c3 Merge pull request #1450 from matrix-org/jryans/defer-cross-signing-setup
Untangle cross-signing and secret storage
2020-08-28 12:40:28 +01:00
J. Ryan Stinnett 7a2416bb6d Add callback for app to cache secret storage key
This is useful when e.g. resetting both secret storage and cross-signing
together, as it avoids prompting for the secret storage key that was just
created.
2020-08-28 12:07:35 +01:00
J. Ryan Stinnett a1528e9e33 Change log messages for uniqueness 2020-08-28 11:38:19 +01:00
J. Ryan Stinnett 71cc4d535e Clean up comment 2020-08-28 11:36:02 +01:00
J. Ryan Stinnett c06723df3d Always store cross-signing keys when new 4S key ID 2020-08-28 11:35:25 +01:00
J. Ryan Stinnett 06b285c013 Add comment about trap with multiple devices 2020-08-28 11:11:51 +01:00
J. Ryan Stinnett 49c06ef0ca Check cross-signing reset branch first
If we're resetting keys, we have to check that branch first.
2020-08-28 11:00:10 +01:00
J. Ryan Stinnett 5f92357fec Tweak code style 2020-08-27 17:32:46 +01:00
J. Ryan Stinnett 3e0dd3d918 Copy docs to client.js as well 2020-08-27 14:09:12 +01:00
J. Ryan Stinnett f19d76b08d Untangle cross-signing and secret storage
This untangles cross-signing and secret storage setup into separate path that
can be invoked independently. There is no functional change with this patch, but
instead this just separates one giant monster API into two.

Part of https://github.com/vector-im/element-web/issues/13895
2020-08-27 13:32:54 +01:00
J. Ryan Stinnett 5ef5412a55 Move storage methods up to constructor 2020-08-25 19:27:26 +01:00
J. Ryan Stinnett e88a384aa7 Update and reformat client.js docs copy 2020-08-25 12:56:30 +01:00
J. Ryan Stinnett 9067feeafb Update comment on 4S signatures
As part of changing 4S to symmetric encryption, we no longer sign the 4S key
with the cross-signing MSK.
2020-08-25 12:56:25 +01:00
10 changed files with 433 additions and 163 deletions
+1 -1
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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",
+3 -5
View File
@@ -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) {
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+7
View File
@@ -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
View File
@@ -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);
+6
View File
@@ -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>;
}
/**