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

Track SSKs for users

and verify our own against our locally stored private part
This commit is contained in:
David Baker
2019-02-01 13:04:21 +00:00
parent 1f77cc6d1a
commit 1d58a64ee1
5 changed files with 262 additions and 21 deletions

View File

@@ -1,6 +1,7 @@
{ {
"presets": ["es2015"], "presets": ["es2015", "es2016"],
"plugins": [ "plugins": [
"transform-class-properties",
// this transforms async functions into generator functions, which // this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's // are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015). // transform-regnerator plugin (which is enabled by es2015).

View File

@@ -67,8 +67,10 @@
"devDependencies": { "devDependencies": {
"babel-cli": "^6.18.0", "babel-cli": "^6.18.0",
"babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
"babel-preset-es2016": "^6.24.1",
"browserify": "^16.2.3", "browserify": "^16.2.3",
"browserify-shim": "^3.8.13", "browserify-shim": "^3.8.13",
"eslint": "^5.12.0", "eslint": "^5.12.0",

View File

@@ -23,9 +23,11 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import {EventEmitter} from 'events';
import logger from '../logger'; import logger from '../logger';
import DeviceInfo from './deviceinfo'; import DeviceInfo from './deviceinfo';
import SskInfo from './sskinfo';
import olmlib from './olmlib'; import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -60,8 +62,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
/** /**
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
*/ */
export default class DeviceList { export default class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, sessionStore, olmDevice) { constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
super();
this._cryptoStore = cryptoStore; this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
@@ -72,6 +76,11 @@ export default class DeviceList {
// } // }
this._devices = {}; this._devices = {};
// userId -> {
// [key info]
// }
this._ssks = {};
// map of identity keys to the user who owns it // map of identity keys to the user who owns it
this._userByIdentityKey = {}; this._userByIdentityKey = {};
@@ -122,12 +131,14 @@ export default class DeviceList {
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken(); this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
this._cryptoStore.storeEndToEndDeviceData({ this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices, devices: this._devices,
self_signing_keys: this._ssks,
trackingStatus: this._deviceTrackingStatus, trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken, syncToken: this._syncToken,
}, txn); }, txn);
shouldDeleteSessionStore = true; shouldDeleteSessionStore = true;
} else { } else {
this._devices = deviceData ? deviceData.devices : {}, this._devices = deviceData ? deviceData.devices : {},
this._ssks = deviceData ? deviceData.self_signing_keys || {} : {};
this._deviceTrackingStatus = deviceData ? this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {}; deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null; this._syncToken = deviceData ? deviceData.syncToken : null;
@@ -224,6 +235,7 @@ export default class DeviceList {
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({ this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices, devices: this._devices,
self_signing_keys: this._ssks,
trackingStatus: this._deviceTrackingStatus, trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken, syncToken: this._syncToken,
}, txn); }, txn);
@@ -357,6 +369,21 @@ export default class DeviceList {
return this._devices[userId]; return this._devices[userId];
} }
getRawStoredSskForUser(userId) {
return this._ssks[userId];
}
getStoredSskForUser(userId) {
if (!this._ssks[userId]) return null;
return SskInfo.fromStorage(this._ssks[userId]);
}
storeSskForUser(userId, ssk) {
this._ssks[userId] = ssk;
this._dirty = true;
}
/** /**
* Get the stored keys for a single device * Get the stored keys for a single device
* *
@@ -584,6 +611,10 @@ export default class DeviceList {
} }
} }
setRawStoredSskForUser(userId, ssk) {
this._ssks[userId] = ssk;
}
/** /**
* Fire off download update requests for the given users, and update the * Fire off download update requests for the given users, and update the
* device list tracking status for them, and the * device list tracking status for them, and the
@@ -747,6 +778,7 @@ class DeviceListUpdateSerialiser {
downloadUsers, opts, downloadUsers, opts,
).then((res) => { ).then((res) => {
const dk = res.device_keys || {}; const dk = res.device_keys || {};
const ssks = res.self_signing_keys || {};
// do each user in a separate promise, to avoid wedging the CPU // do each user in a separate promise, to avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158) // (https://github.com/vector-im/riot-web/issues/3158)
@@ -756,7 +788,7 @@ class DeviceListUpdateSerialiser {
let prom = Promise.resolve(); let prom = Promise.resolve();
for (const userId of downloadUsers) { for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => { prom = prom.delay(5).then(() => {
return this._processQueryResponseForUser(userId, dk[userId]); return this._processQueryResponseForUser(userId, dk[userId], ssks[userId]);
}); });
} }
@@ -780,9 +812,11 @@ class DeviceListUpdateSerialiser {
return deferred.promise; return deferred.promise;
} }
async _processQueryResponseForUser(userId, response) { async _processQueryResponseForUser(userId, dk_response, ssk_response) {
logger.log('got keys for ' + userId + ':', response); logger.log('got device keys for ' + userId + ':', dk_response);
logger.log('got self-signing keys for ' + userId + ':', ssk_response);
{
// map from deviceid -> deviceinfo for this user // map from deviceid -> deviceinfo for this user
const userStore = {}; const userStore = {};
const devs = this._deviceList.getRawStoredDevicesForUser(userId); const devs = this._deviceList.getRawStoredDevicesForUser(userId);
@@ -794,10 +828,10 @@ class DeviceListUpdateSerialiser {
} }
await _updateStoredDeviceKeysForUser( await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {}, this._olmDevice, userId, userStore, dk_response || {},
); );
// put the updates into thr object that will be returned as our results // put the updates into the object that will be returned as our results
const storage = {}; const storage = {};
Object.keys(userStore).forEach((deviceId) => { Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage(); storage[deviceId] = userStore[deviceId].toStorage();
@@ -805,6 +839,22 @@ class DeviceListUpdateSerialiser {
this._deviceList._setRawStoredDevicesForUser(userId, storage); this._deviceList._setRawStoredDevicesForUser(userId, storage);
} }
// now do the same for the self-signing key
{
const ssk = this._deviceList.getRawStoredSskForUser(userId) || {};
const updated = await _updateStoredSelfSigningKeyForUser(
this._olmDevice, userId, ssk, ssk_response || {},
);
this._deviceList.setRawStoredSskForUser(userId, ssk);
// NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted
if (updated) this._deviceList.emit('userSskUpdated', userId);
}
}
} }
@@ -854,6 +904,49 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
return updated; return updated;
} }
async function _updateStoredSelfSigningKeyForUser(
_olmDevice, userId, userStore, userResult,
) {
let updated = false;
if (userResult.user_id !== userId) {
logger.warn("Mismatched user_id " + userResult.user_id +
" in self-signing key from " + userId);
return;
}
if (!userResult || !userResult.usage.includes('self_signing')) {
logger.warn("Self-signing key for " + userId + " does not include 'self_signing' usage: ignoring");
return;
}
const keyCount = Object.keys(userResult.keys).length;
if (keyCount !== 1) {
logger.warn(
"Self-signing key block for " + userId + " has " + keyCount + " keys: expected exactly 1. Ignoring.",
);
return;
}
let oldKeyId = null;
let oldKey = null;
if (userStore.keys && Object.keys(userStore.keys).length > 0) {
oldKeyId = Object.keys(userStore.keys)[0];
oldKey = userStore.keys[oldKeyId];
}
const newKeyId = Object.keys(userResult.keys)[0];
const newKey = userResult.keys[newKeyId];
if (oldKeyId !== newKeyId || oldKey !== newKey) {
updated = true;
logger.info("New self-signing key detected for " + userId + ": " + newKeyId + ", was previously " + oldKeyId);
userStore.user_id = userResult.user_id;
userStore.usage = userResult.usage;
userStore.keys = userResult.keys;
// reset verification status since its now a new key
userStore.verified = SskInfo.SskVerification.UNVERIFIED;
}
return updated;
}
/* /*
* Process a device in a /query response, and add it to the userStore * Process a device in a /query response, and add it to the userStore
* *

View File

@@ -31,6 +31,7 @@ const OlmDevice = require("./OlmDevice");
const olmlib = require("./olmlib"); const olmlib = require("./olmlib");
const algorithms = require("./algorithms"); const algorithms = require("./algorithms");
const DeviceInfo = require("./deviceinfo"); const DeviceInfo = require("./deviceinfo");
import SskInfo from './sskinfo';
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceList = require('./DeviceList').default; const DeviceList = require('./DeviceList').default;
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
@@ -102,6 +103,8 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
*/ */
export default function Crypto(baseApis, sessionStore, userId, deviceId, export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList, verificationMethods) { clientStore, cryptoStore, roomList, verificationMethods) {
this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this);
this._baseApis = baseApis; this._baseApis = baseApis;
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
this._userId = userId; this._userId = userId;
@@ -140,6 +143,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._deviceList = new DeviceList( this._deviceList = new DeviceList(
baseApis, cryptoStore, sessionStore, this._olmDevice, baseApis, cryptoStore, sessionStore, this._olmDevice,
); );
// XXX: This isn't removed at any point, but then none of the event listeners
// this class sets seem to be removed at any point... :/
this._deviceList.on('userSskUpdated', this._onDeviceListUserSskUpdated);
// the last time we did a check for the number of one-time-keys on the // the last time we did a check for the number of one-time-keys on the
// server. // server.
@@ -255,6 +261,59 @@ Crypto.prototype.init = async function() {
this._checkAndStartKeyBackup(); this._checkAndStartKeyBackup();
}; };
/*
* Event handler for DeviceList's userNewDevices event
*/
Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) {
if (userId === this._userId) {
// If we see an update to our own SSK, check it against the SSK we have and,
// if it matches, mark it as verified
// First, get the pubkey of the one we can see
const seenSsk = this._deviceList.getStoredSskForUser(userId);
if (!seenSsk) {
logger.error("Got SSK update event for user " + userId + " but no new SSK found!");
return;
}
const seenPubkey = seenSsk.getFingerprint();
// 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);
} else {
logger.info(
"Published self-signing key DOES NOT match local copy! Local: " +
localPubkey + ", published: " + seenPubkey,
);
}
}
};
/** /**
* Check the server for an active key backup and * Check the server for an active key backup and
* if one is present and has a valid signature from * if one is present and has a valid signature from
@@ -719,6 +778,16 @@ Crypto.prototype.saveDeviceList = function(delay) {
return this._deviceList.saveIfDirty(delay); return this._deviceList.saveIfDirty(delay);
}; };
Crypto.prototype.setSskVerification = async function(userId, verified) {
const ssk = this._deviceList.getRawStoredSskForUser(userId);
if (!ssk) {
throw new Error("No self-signing key found for user " + userId);
}
ssk.verified = verified;
this._deviceList.storeSskForUser(userId, ssk)
this._deviceList.saveIfDirty();
};
/** /**
* Update the blocked/verified state of the given device * Update the blocked/verified state of the given device
* *

76
src/crypto/sskinfo.js Normal file
View File

@@ -0,0 +1,76 @@
/*
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;
}
getFingerprint() {
return Object.values(this.keys)[0];
}
isVerified() {
return this.verified == SskInfo.SskVerification.VERIFIED;
};
isUnverified() {
return this.verified == SskInfo.SskVerification.UNVERIFIED;
};
}