1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Rework to hold cross-signing keys in JS SDK as needed

This commit is contained in:
J. Ryan Stinnett
2019-12-10 16:39:42 +00:00
parent 44dd674dab
commit 6942e3467b
5 changed files with 110 additions and 156 deletions

View File

@@ -246,18 +246,11 @@ describe("Secrets", function() {
}); });
it("bootstraps when no storage or cross-signing keys locally", async function() { it("bootstraps when no storage or cross-signing keys locally", async function() {
let keys = {};
const bob = await makeTestClient( const bob = await makeTestClient(
{ {
userId: "@bob:example.com", userId: "@bob:example.com",
deviceId: "bob1", deviceId: "bob1",
}, },
{
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
},
},
); );
bob.uploadDeviceSigningKeys = async () => {}; bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {}; bob.uploadKeySignatures = async () => {};
@@ -287,7 +280,6 @@ describe("Secrets", function() {
const storagePublicKey = decryption.generate_key(); const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key(); const storagePrivateKey = decryption.get_private_key();
let crossSigningKeys = {};
const bob = await makeTestClient( const bob = await makeTestClient(
{ {
userId: "@bob:example.com", userId: "@bob:example.com",
@@ -295,8 +287,6 @@ describe("Secrets", function() {
}, },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: request => { getSecretStorageKey: request => {
const defaultKeyId = bob.getDefaultSecretStorageKeyId(); const defaultKeyId = bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]); expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
@@ -318,6 +308,7 @@ describe("Secrets", function() {
]); ]);
this.emit("accountData", event); this.emit("accountData", event);
}; };
bob._crypto.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo; const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage; const secretStorage = bob._crypto._secretStorage;
@@ -328,6 +319,10 @@ describe("Secrets", function() {
}); });
// Clear local cross-signing keys and read from secret storage // Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {}; crossSigning.keys = {};
await bob.bootstrapSecretStorage(); await bob.bootstrapSecretStorage();

View File

@@ -181,7 +181,8 @@ function keyFromRecoverySession(session, decryptionKey) {
* The cross-signing API is currently UNSTABLE and may change without notice. * The cross-signing API is currently UNSTABLE and may change without notice.
* *
* @param {function} [opts.cryptoCallbacks.getCrossSigningKey] * @param {function} [opts.cryptoCallbacks.getCrossSigningKey]
* Optional (required for cross-signing). Function to call when a cross-signing private key is needed. * Optional. Function to call when a cross-signing private key is needed.
* Secure Secret Storage will be used by default if this is unset.
* Args: * Args:
* {string} type The type of key needed. Will be one of "master", * {string} type The type of key needed. Will be one of "master",
* "self_signing", or "user_signing" * "self_signing", or "user_signing"
@@ -193,8 +194,8 @@ function keyFromRecoverySession(session, decryptionKey) {
* UInt8Array or rejects with an error. * UInt8Array or rejects with an error.
* *
* @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys] * @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys]
* Optional (required for cross-signing). Called when new private keys * Optional. Called when new private keys for cross-signing need to be saved.
* for cross-signing need to be saved. * Secure Secret Storage will be used by default if this is unset.
* Args: * Args:
* {object} keys the private keys to save. Map of key name to private key * {object} keys the private keys to save. Map of key name to private key
* as a UInt8Array. The getPrivateKey callback above will be called * as a UInt8Array. The getPrivateKey callback above will be called
@@ -298,7 +299,7 @@ function MatrixClient(opts) {
this._cryptoStore = opts.cryptoStore; this._cryptoStore = opts.cryptoStore;
this._sessionStore = opts.sessionStore; this._sessionStore = opts.sessionStore;
this._verificationMethods = opts.verificationMethods; this._verificationMethods = opts.verificationMethods;
this._cryptoCallbacks = opts.cryptoCallbacks; this._cryptoCallbacks = opts.cryptoCallbacks || {};
this._forceTURN = opts.forceTURN || false; this._forceTURN = opts.forceTURN || false;
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;

View File

@@ -115,8 +115,8 @@ export class CrossSigningInfo extends EventEmitter {
*/ */
isStoredInSecretStorage(secretStorage) { isStoredInSecretStorage(secretStorage) {
let stored = true; let stored = true;
for (const name of ["master", "self_signing", "user_signing"]) { for (const type of ["master", "self_signing", "user_signing"]) {
stored &= secretStorage.isStored(`m.cross_signing.${name}`, false); stored &= secretStorage.isStored(`m.cross_signing.${type}`, false);
} }
return stored; return stored;
} }
@@ -126,13 +126,13 @@ export class CrossSigningInfo extends EventEmitter {
* typically called in conjunction with the creation of new cross-signing * typically called in conjunction with the creation of new cross-signing
* keys. * keys.
* *
* @param {object} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data * @param {SecretStorage} secretStorage The secret store using account data
*/ */
async storeInSecretStorage(secretStorage) { static async storeInSecretStorage(keys, secretStorage) {
const getKey = this._callbacks.getCrossSigningKey; for (const type of Object.keys(keys)) {
for (const name of ["master", "self_signing", "user_signing"]) { const encodedKey = encodeBase64(keys[type]);
const encodedKey = encodeBase64(await getKey(name)); await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
await secretStorage.store(`m.cross_signing.${name}`, encodedKey);
} }
} }
@@ -140,54 +140,14 @@ export class CrossSigningInfo extends EventEmitter {
* Get private keys from secret storage created by some other device. This * Get private keys from secret storage created by some other device. This
* also passes the private keys to the app-specific callback. * also passes the private keys to the app-specific callback.
* *
* @param {string} type The type of key to get. One of "master",
* "self_signing", or "user_signing".
* @param {SecretStorage} secretStorage The secret store using account data * @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key
*/ */
async getFromSecretStorage(secretStorage) { static async getFromSecretStorage(type, secretStorage) {
if (!this._callbacks.saveCrossSigningKeys) { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
throw new Error("No saveCrossSigningKeys callback supplied"); return decodeBase64(encodedKey);
}
// Retrieve private keys from secret storage
const privateKeys = {};
for (const name of ["master", "self_signing", "user_signing"]) {
const encodedKey = await secretStorage.get(`m.cross_signing.${name}`);
privateKeys[name] = decodeBase64(encodedKey);
}
// Regenerate public keys from private keys
// XXX: Do we want to _also_ download public keys from the homeserver to
// verify they agree...?
// See also https://github.com/vector-im/riot-web/issues/11558
const signings = {};
const publicKeys = {};
const keys = {};
try {
for (const name of ["master", "self_signing", "user_signing"]) {
signings[name] = new global.Olm.PkSigning();
publicKeys[name] = signings[name].init_with_seed(privateKeys[name]);
keys[name] = {
user_id: this.userId,
usage: [name],
keys: {
['ed25519:' + publicKeys[name]]: publicKeys[name],
},
};
if (name !== "master") {
pkSign(
keys[name], signings["master"],
this.userId, publicKeys["master"],
);
}
}
} finally {
for (const signing of Object.values(signings)) {
signing.free();
}
}
// Save public keys locally and private keys via app callback
Object.assign(this.keys, keys);
this._callbacks.saveCrossSigningKeys(privateKeys);
} }
/** /**

View File

@@ -167,8 +167,6 @@ export default class SecretStorage extends EventEmitter {
return keyInfo && keyInfo.getContent(); return keyInfo && keyInfo.getContent();
} }
// TODO: need a function to get all the secret keys
/** /**
* Store an encrypted secret on the server * Store an encrypted secret on the server
* *

View File

@@ -245,13 +245,20 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._verificationTransactions = new Map(); this._verificationTransactions = new Map();
this._crossSigningInfo = new CrossSigningInfo( const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
userId, this._baseApis._cryptoCallbacks,
); this._crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks);
this._secretStorage = new SecretStorage( this._secretStorage = new SecretStorage(
baseApis, this._baseApis._cryptoCallbacks, this._crossSigningInfo, baseApis, cryptoCallbacks, this._crossSigningInfo,
); );
// Assuming no app-supplied callback, default to getting from SSSS.
if (!cryptoCallbacks.getCrossSigningKey) {
cryptoCallbacks.getCrossSigningKey = async (type) => {
return CrossSigningInfo.getFromSecretStorage(type, this._secretStorage);
};
}
} }
utils.inherits(Crypto, EventEmitter); utils.inherits(Crypto, EventEmitter);
@@ -377,55 +384,75 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// key with the cross-signing master key. The cross-signing master key is also used // 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 // to verify the signature on the SSSS default key when adding secrets, so we
// effectively need it for both reading and writing secrets. // effectively need it for both reading and writing secrets.
let crossSigningKeysReset = false; let crossSigningPrivateKeys = {};
if (
!this._crossSigningInfo.getId() || // If we happen to reset cross-signing keys here, then we want access to the
!await this._baseApis._cryptoCallbacks.getCrossSigningKey("master") // cross-signing private keys, but only for the scope of this method, so we
) { // use temporary callbacks to weave the them through the various APIs.
logger.log( const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
"Cross-signing public and/or private keys not found on device, " +
"checking secret storage for private keys", try {
); if (
if (this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage)) { !this._crossSigningInfo.getId() ||
logger.log("Cross-signing private keys found in secret storage"); !this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage)
await this._getCrossSigningKeysFromSecretStorage(); ) {
} else {
logger.log( logger.log(
"Cross-signing private keys not found in secret storage, " + "Cross-signing public and/or private keys not found, " +
"creating new keys", "checking secret storage for private keys",
); );
await this.resetCrossSigningKeys( if (this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage)) {
CrossSigningLevel.MASTER, logger.log("Cross-signing private keys found in secret storage");
{ authUploadDeviceSigningKeys }, await this.checkOwnCrossSigningTrust();
); } else {
crossSigningKeysReset = true; logger.log(
"Cross-signing private keys not found in secret storage, " +
"creating new keys",
);
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
keys => crossSigningPrivateKeys = keys;
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
await this.resetCrossSigningKeys(
CrossSigningLevel.MASTER,
{ authUploadDeviceSigningKeys },
);
}
} }
}
// Check if Secure Secret Storage has a default key. If we don't have one, create the // Check if Secure Secret Storage has a default key. If we don't have one, create
// default key (which will also be signed by the cross-signing master key). // the default key (which will also be signed by the cross-signing master key).
if (!this.hasSecretStorageKey()) { if (!this.hasSecretStorageKey()) {
logger.log("Secret storage default key not found, creating new key"); logger.log("Secret storage default key not found, creating new key");
const keyOptions = await createSecretStorageKey(); const keyOptions = await createSecretStorageKey();
const newKeyId = await this.addSecretStorageKey( const newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1, SECRET_STORAGE_ALGORITHM_V1,
keyOptions, keyOptions,
); );
await this.setDefaultSecretStorageKeyId(newKeyId); await this.setDefaultSecretStorageKeyId(newKeyId);
} }
// If cross-signing keys were reset, store them in Secure 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 // This is done in a separate step so we can ensure secret storage has its
// own key first. // own key first.
// XXX: We need to think about how to re-do these steps if they fail. // XXX: We need to think about how to re-do these steps if they fail.
if (crossSigningKeysReset) { // See also https://github.com/vector-im/riot-web/issues/11635
logger.log("Storing cross-signing private keys in secret storage"); if (crossSigningPrivateKeys) {
// SSSS expects its keys to be signed by cross-signing master key. logger.log("Storing cross-signing private keys in secret storage");
// Since we have just reset cross-signing keys, we need to re-sign the // SSSS expects its keys to be signed by cross-signing master key.
// SSSS default key with the new cross-signing master key so that the // Since we have just reset cross-signing keys, we need to re-sign the
// following storage step can proceed. // SSSS default key with the new cross-signing master key so that the
await this._secretStorage.signKey(); // following storage step can proceed.
await this._crossSigningInfo.storeInSecretStorage(this._secretStorage); await this._secretStorage.signKey();
// Assuming no app-supplied callback, default to storing in SSSS.
if (!appCallbacks.saveCrossSigningKeys) {
await CrossSigningInfo.storeInSecretStorage(
crossSigningPrivateKeys,
this._secretStorage,
);
}
}
} finally {
this._baseApis._cryptoCallbacks = appCallbacks;
} }
logger.log("Secure Secret Storage ready"); logger.log("Secure Secret Storage ready");
@@ -559,41 +586,6 @@ Crypto.prototype.resetCrossSigningKeys = async function(level, {
logger.info("Cross-signing key reset complete"); logger.info("Cross-signing key reset complete");
}; };
/**
* If cross-signing keys are known to exist in secret storage, this will get
* them and store them on this device as trusted.
*/
Crypto.prototype._getCrossSigningKeysFromSecretStorage = async function() {
logger.info("Getting cross-signing keys from secret storage");
// Copy old keys (usually empty) in case we need to revert
const oldKeys = Object.assign({}, this._crossSigningInfo.keys);
try {
await this._crossSigningInfo.getFromSecretStorage(this._secretStorage);
// XXX: Do we also need to sign the cross-signing master key with the
// device key as in `resetCrossSigningKeys`?
// 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(
"Getting cross-signing keys from secret storage failed, " +
"revert to previous keys", e,
);
this._crossSigningInfo.keys = oldKeys;
throw e;
}
this._baseApis.emit("crossSigning.keysChanged", {});
await this._afterCrossSigningLocalKeyChange();
logger.info("Cross-signing keys restored from secret storage");
};
/** /**
* Run various follow-up actions after cross-signing keys have changed locally * Run various follow-up actions after cross-signing keys have changed locally
* (either by resetting the keys for the account or bye getting them from secret * (either by resetting the keys for the account or bye getting them from secret
@@ -817,10 +809,10 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
} }
const seenPubkey = newCrossSigning.getId(); const seenPubkey = newCrossSigning.getId();
const changed = this._crossSigningInfo.getId() !== seenPubkey; const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
if (changed) { if (masterChanged) {
// try to get the private key if the master key changed // try to get the private key if the master key changed
logger.info("Got new master key", seenPubkey); logger.info("Got new master public key", seenPubkey);
let signing = null; let signing = null;
try { try {
@@ -828,6 +820,9 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
'master', seenPubkey, 'master', seenPubkey,
); );
signing = ret[1]; signing = ret[1];
if (!signing) {
throw new Error("Cross-signing master private key not available");
}
} finally { } finally {
signing.free(); signing.free();
} }
@@ -862,7 +857,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
} }
if (changed) { if (masterChanged) {
await this._signObject(this._crossSigningInfo.keys.master); await this._signObject(this._crossSigningInfo.keys.master);
keySignatures[this._crossSigningInfo.getId()] keySignatures[this._crossSigningInfo.getId()]
= this._crossSigningInfo.keys.master; = this._crossSigningInfo.keys.master;
@@ -874,6 +869,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
if (masterChanged) {
this._baseApis.emit("crossSigning.keysChanged", {});
await this._afterCrossSigningLocalKeyChange();
}
// Now we may be able to trust our key backup // Now we may be able to trust our key backup
await this.checkKeyBackup(); await this.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign // FIXME: if we previously trusted the backup, should we automatically sign