1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

sign backups with master key

This commit is contained in:
Hubert Chathi
2019-07-03 15:15:41 -04:00
parent 4356603665
commit c5caf8f8f4
4 changed files with 58 additions and 384 deletions

View File

@@ -1209,16 +1209,8 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
throw new Error("End-to-end encryption disabled");
}
let decryption;
let encryption;
let signing;
const decryption = new global.Olm.PkDecryption();
try {
decryption = new global.Olm.PkDecryption();
encryption = new global.Olm.PkEncryption();
if (global.Olm.PkSigning) {
signing = new global.Olm.PkSigning();
}
let publicKey;
const authData = {};
if (password) {
@@ -1231,64 +1223,15 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
}
authData.public_key = publicKey;
encryption.set_recipient_key(publicKey);
const returnInfo = {
return {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: authData,
recovery_key: encodeRecoveryKey(decryption.get_private_key()),
accountKeys: null,
};
if (signing) {
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getAccountKeys(txn, (keys) => {
returnInfo.accountKeys = keys;
});
},
);
if (!returnInfo.accountKeys) {
const sskSeed = signing.generate_seed();
const uskSeed = signing.generate_seed();
returnInfo.accountKeys = {
self_signing_key_seed: Buffer.from(sskSeed).toString('base64'),
user_signing_key_seed: Buffer.from(uskSeed).toString('base64'),
};
}
// put the encrypted version of the seed in the auth data to upload
// XXX: our encryption really should support encrypting binary data.
authData.self_signing_key_seed = encryption.encrypt(
returnInfo.accountKeys.self_signing_key_seed,
);
// also keep the public part there
returnInfo.ssk_public = signing.init_with_seed(
Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64'),
);
signing.free();
// same for the USK
authData.user_signing_key_seed = encryption.encrypt(
returnInfo.accountKeys.user_signing_key_seed,
);
returnInfo.usk_public = signing.init_with_seed(
Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64'),
);
signing.free();
// we don't save these keys back to the store yet: we'll do that when (if) we
// actually create the backup
}
return returnInfo;
} finally {
if (decryption) decryption.free();
if (encryption) encryption.free();
if (signing) signing.free();
decryption.free();
}
};
@@ -1297,11 +1240,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
* from prepareKeyBackupVersion.
*
* @param {object} info Info object from prepareKeyBackupVersion
* @param {object} auth Auth object for UI auth
* @param {string} replacesSsk If the SSK is being replaced, the ID of the old key
* @returns {Promise<object>} Object with 'version' param indicating the version created
*/
MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, replacesSsk) {
MatrixClient.prototype.createKeyBackupVersion = async function(info) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
@@ -1311,73 +1252,25 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, repla
auth_data: info.auth_data,
};
const uskInfo = {
user_id: this.credentials.userId,
usage: ['user_signing'],
keys: {
['ed25519:' + info.usk_public]: info.usk_public,
},
};
// sign the USK with the SSK
pkSign(
uskInfo,
Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'),
this.credentials.userId,
);
// Now sig the backup auth data. Do it as this device first because crypto._signObject
// is dumb and bluntly replaces the whole signatures block...
// this can probably go away very soon in favour of just signing with the SSK.
await this._crypto._signObject(data.auth_data);
// now also sign the auth data with the SSK
pkSign(
data.auth_data,
Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'),
this.credentials.userId,
if (this._crypto._crossSigningInfo.getId()) {
// now also sign the auth data with the SSK
await this._crypto._crossSigningInfo.signObject(data.auth_data, "master");
}
const res = await this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
);
const keys = {
self_signing_key: {
user_id: this.credentials.userId,
usage: ['self_signing'],
keys: {
['ed25519:' + info.ssk_public]: info.ssk_public,
},
replaces: replacesSsk,
},
user_signing_key: uskInfo,
auth,
};
return this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
// store the newly generated account keys
this._cryptoStore.storeAccountKeys(txn, info.accountKeys);
},
).then(() => {
// re-check the SSK in the device store if necessary
return this._crypto.checkOwnSskTrust();
}).then(() => {
// upload the public part of the account keys
return this.uploadDeviceSigningKeys(keys);
}).then(() => {
return this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
);
}).then((res) => {
this.enableKeyBackup({
algorithm: info.algorithm,
auth_data: info.auth_data,
version: res.version,
});
return res;
}).then(() => {
// upload signatures between the SSK & this device
return this._crypto.uploadDeviceKeySignatures();
this.enableKeyBackup({
algorithm: info.algorithm,
auth_data: info.auth_data,
version: res.version,
});
return res;
};
MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
@@ -1483,7 +1376,7 @@ MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
);
};
MatrixClient.prototype._restoreKeyBackup = async function(
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, backupInfo,
) {
if (this._crypto === null) {
@@ -1492,46 +1385,14 @@ MatrixClient.prototype._restoreKeyBackup = async function(
let totalKeyCount = 0;
let keys = [];
const path = this._makeKeyBackupPath(
targetRoomId, targetSessionId, backupInfo.version,
);
const decryption = new global.Olm.PkDecryption();
let backupPubKey;
try {
backupPubKey = decryption.init_with_private_key(privKey);
// decrypt the account keys from the backup info if there are any
// fetch the old ones first so we don't lose info if only one of them is in the backup
let accountKeys;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getAccountKeys(txn, (keys) => {
accountKeys = keys || {};
});
},
);
if (backupInfo.auth_data.self_signing_key_seed) {
accountKeys.self_signing_key_seed = decryption.decrypt(
backupInfo.auth_data.self_signing_key_seed.ephemeral,
backupInfo.auth_data.self_signing_key_seed.mac,
backupInfo.auth_data.self_signing_key_seed.ciphertext,
);
}
if (backupInfo.auth_data.user_signing_key_seed) {
accountKeys.user_signing_key_seed = decryption.decrypt(
backupInfo.auth_data.user_signing_key_seed.ephemeral,
backupInfo.auth_data.user_signing_key_seed.mac,
backupInfo.auth_data.user_signing_key_seed.ciphertext,
);
}
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeAccountKeys(txn, accountKeys);
},
);
await this._crypto.checkOwnSskTrust();
} catch(e) {
decryption.free();
throw e;
@@ -1544,16 +1405,9 @@ MatrixClient.prototype._restoreKeyBackup = async function(
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
}
// start by signing this device from the SSK now we have it
return this._crypto.uploadDeviceKeySignatures().then(() => {
// Now fetch the encrypted keys
const path = this._makeKeyBackupPath(
targetRoomId, targetSessionId, backupInfo.version,
);
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
);
}).then((res) => {
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
).then((res) => {
if (res.rooms) {
for (const [roomId, roomData] of Object.entries(res.rooms)) {
if (!roomData.sessions) continue;

View File

@@ -40,13 +40,15 @@ async function getPrivateKey(self, type, check) {
// FIXME: the key needs to be interpreted?
const signing = new global.Olm.PkSigning();
const pubkey = signing.init_with_seed(key);
error = check(pubkey, signing);
if (error) {
// make sure it agrees with the pubkey that we have
if (pubkey !== getPublicKey(self.keys[type])[1]) {
error = "Key does not match";
logger.error(error);
signing.free();
resolve([null, null]);
} else {
resolve([pubkey, signing]);
}
resolve([pubkey, signing]);
},
cancel: (error) => {
reject(error || new Error("Cancelled"));
@@ -131,14 +133,7 @@ export class CrossSigningInfo extends EventEmitter {
},
};
} else {
[masterPub, masterSigning] = await getPrivateKey(
this, "master", (pubkey) => {
// make sure it agrees with the pubkey that we have
if (pubkey !== getPublicKey(this.keys.master)[1]) {
return "Key does not match";
}
return;
});
[masterPub, masterSigning] = await getPrivateKey(this, "master");
}
if (level & CrossSigningLevel.SELF_SIGNING) {
@@ -258,21 +253,21 @@ export class CrossSigningInfo extends EventEmitter {
}
}
async signObject(data, type) {
const [pubkey, signing] = await getPrivateKey(this, type);
try {
pkSign(data, signing, this.userId, pubkey);
return data;
} finally {
signing.free();
}
}
async signUser(key) {
if (!this.keys.user_signing) {
return;
}
const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => {
// FIXME:
return;
});
try {
const otherMaster = key.keys.master;
pkSign(otherMaster, usk, this.userId, pubkey);
return otherMaster;
} finally {
usk.free();
}
return this.signObject(key.keys.master, "user_signing");
}
async signDevice(userId, device) {
@@ -284,22 +279,14 @@ export class CrossSigningInfo extends EventEmitter {
if (!this.keys.self_signing) {
return;
}
const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => {
// FIXME:
return;
});
try {
const keyObj = {
return this.signObject(
{
algorithms: device.algorithms,
keys: device.keys,
device_id: device.deviceId,
user_id: userId,
};
pkSign(keyObj, ssk, this.userId, pubkey);
return keyObj;
} finally {
ssk.free();
}
}, "self_signing",
);
}
checkUserTrust(userCrossSigning) {

View File

@@ -513,52 +513,10 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
this._baseApis.emit("cross-signing.keysChanged", {});
}
// FIXME:
// Now dig out the account keys and get the pubkey of the one in there
/*
let accountKeys = null;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getAccountKeys(txn, keys => {
accountKeys = keys;
});
},
);
if (!accountKeys || !accountKeys.self_signing_key_seed) {
logger.info(
"Ignoring new self-signing key for us because we have no private part stored",
);
return;
}
let signing;
let localPubkey;
try {
signing = new global.Olm.PkSigning();
localPubkey = signing.init_with_seed(
Buffer.from(accountKeys.self_signing_key_seed, 'base64'),
);
} finally {
if (signing) signing.free();
signing = null;
}
if (!localPubkey) {
logger.error("Unable to compute public key for stored SSK seed");
}
// Finally, are they the same?
if (seenPubkey === localPubkey) {
logger.info("Published self-signing key matches local copy: marking as verified");
this.setSskVerification(userId, SskInfo.SskVerification.VERIFIED);
// Now we may be able to trust our key backup
await this.checkKeyBackup();
} else {
logger.info(
"Published self-signing key DOES NOT match local copy! Local: " +
localPubkey + ", published: " + seenPubkey,
);
}
*/
// Now we may be able to trust our key backup
await this.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign
// the backup with the new key (if not already signed)?
};
/**
@@ -686,21 +644,23 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
// Could be an SSK but just say this is the device ID for backwards compat
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID?
// first check to see if it's from our SSK
const ssk = this._deviceList.getStoredSskForUser(this._userId);
if (ssk && ssk.getKeyId() === keyId) {
sigInfo.self_signing_key = ssk;
// first check to see if it's from our cross-signing key
const crossSigningId = this._crossSigningInfo.getId();
if (crossSigningId === keyId) {
sigInfo.cross_signing_key = crossSigningId;
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
sigInfo.deviceId,
ssk.getFingerprint(),
crossSigningId,
);
sigInfo.valid = true;
} catch (e) {
console.log("Bad signature from ssk " + ssk.getKeyId(), e);
logger.warning(
"Bad signature from cross signing key " + crossSigningId, e,
);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
@@ -745,7 +705,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
return (
s.valid && (
(s.device && s.device.isVerified()) ||
(s.self_signing_key && s.self_signing_key.isVerified())
(s.cross_signing_key)
)
);
});
@@ -843,7 +803,6 @@ Crypto.prototype.uploadDeviceKeys = function() {
user_id: userId,
};
let accountKeys;
return crypto._signObject(deviceKeys).then(() => {
return crypto._baseApis.uploadKeysRequest({
device_keys: deviceKeys,
@@ -855,52 +814,6 @@ Crypto.prototype.uploadDeviceKeys = function() {
});
};
/**
* If a self-signing key is available, uploads the signature of this device from
* the self-signing key
*
* @return {bool} Promise: True if signatures were uploaded or otherwise false
* (eg. if no account keys were available)
*/
Crypto.prototype.uploadDeviceKeySignatures = async function() {
const crypto = this;
const userId = crypto._userId;
const deviceId = crypto._deviceId;
const thisDeviceKey = {
algorithms: crypto._supportedAlgorithms,
device_id: deviceId,
keys: crypto._deviceKeys,
user_id: userId,
};
let accountKeys;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getAccountKeys(txn, keys => {
accountKeys = keys;
},
);
});
if (!accountKeys || !accountKeys.self_signing_key_seed) return false;
// Sign this device with the SSK
pkSign(
thisDeviceKey,
Buffer.from(accountKeys.self_signing_key_seed, 'base64'),
userId,
);
const content = {
[userId]: {
[deviceId]: thisDeviceKey,
},
};
await crypto._baseApis.uploadKeySignatures(content);
return true;
};
/**
* Stores the current one_time_key count which will be handled later (in a call of
* onSyncCompleted). The count is e.g. coming from a /sync response.

View File

@@ -1,80 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module crypto/sskinfo
*/
/**
* Information about a user's self-signing key
*
* @constructor
* @alias module:crypto/sskinfo
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/sskinfo.SskVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*/
export default class SskInfo {
constructor() {
this.keys = {};
this.verified = SskInfo.SskVerification.UNVERIFIED;
//this.known = false; // is this useful?
this.unsigned = {};
}
/**
* @enum
*/
static SskVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
static fromStorage(obj) {
const res = new SskInfo();
for (const [prop, val] of Object.entries(obj)) {
res[prop] = val;
}
return res;
}
getKeyId() {
return Object.keys(this.keys)[0];
}
getFingerprint() {
return Object.values(this.keys)[0];
}
isVerified() {
return this.verified == SskInfo.SskVerification.VERIFIED;
}
isUnverified() {
return this.verified == SskInfo.SskVerification.UNVERIFIED;
}
}