You've already forked matrix-js-sdk
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:
3
.babelrc
3
.babelrc
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"presets": ["es2015", "es2016"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
// this transforms async functions into generator functions, which
|
||||
// are then made to use the regenerator module by babel's
|
||||
// transform-regnerator plugin (which is enabled by es2015).
|
||||
|
||||
@@ -67,8 +67,10 @@
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"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-preset-es2015": "^6.18.0",
|
||||
"babel-preset-es2016": "^6.24.1",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^5.12.0",
|
||||
|
||||
@@ -23,9 +23,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import SskInfo from './sskinfo';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
@@ -60,8 +62,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList {
|
||||
export default class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
|
||||
super();
|
||||
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._sessionStore = sessionStore;
|
||||
|
||||
@@ -72,6 +76,11 @@ export default class DeviceList {
|
||||
// }
|
||||
this._devices = {};
|
||||
|
||||
// userId -> {
|
||||
// [key info]
|
||||
// }
|
||||
this._ssks = {};
|
||||
|
||||
// map of identity keys to the user who owns it
|
||||
this._userByIdentityKey = {};
|
||||
|
||||
@@ -122,12 +131,14 @@ export default class DeviceList {
|
||||
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
|
||||
this._cryptoStore.storeEndToEndDeviceData({
|
||||
devices: this._devices,
|
||||
self_signing_keys: this._ssks,
|
||||
trackingStatus: this._deviceTrackingStatus,
|
||||
syncToken: this._syncToken,
|
||||
}, txn);
|
||||
shouldDeleteSessionStore = true;
|
||||
} else {
|
||||
this._devices = deviceData ? deviceData.devices : {},
|
||||
this._ssks = deviceData ? deviceData.self_signing_keys || {} : {};
|
||||
this._deviceTrackingStatus = deviceData ?
|
||||
deviceData.trackingStatus : {};
|
||||
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||
@@ -224,6 +235,7 @@ export default class DeviceList {
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.storeEndToEndDeviceData({
|
||||
devices: this._devices,
|
||||
self_signing_keys: this._ssks,
|
||||
trackingStatus: this._deviceTrackingStatus,
|
||||
syncToken: this._syncToken,
|
||||
}, txn);
|
||||
@@ -357,6 +369,21 @@ export default class DeviceList {
|
||||
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
|
||||
*
|
||||
@@ -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
|
||||
* device list tracking status for them, and the
|
||||
@@ -747,6 +778,7 @@ class DeviceListUpdateSerialiser {
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
|
||||
// do each user in a separate promise, to avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
@@ -756,7 +788,7 @@ class DeviceListUpdateSerialiser {
|
||||
let prom = Promise.resolve();
|
||||
for (const userId of downloadUsers) {
|
||||
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;
|
||||
}
|
||||
|
||||
async _processQueryResponseForUser(userId, response) {
|
||||
logger.log('got keys for ' + userId + ':', response);
|
||||
async _processQueryResponseForUser(userId, dk_response, ssk_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
|
||||
const userStore = {};
|
||||
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||
@@ -794,10 +828,10 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
|
||||
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 = {};
|
||||
Object.keys(userStore).forEach((deviceId) => {
|
||||
storage[deviceId] = userStore[deviceId].toStorage();
|
||||
@@ -805,6 +839,22 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
|
||||
@@ -31,6 +31,7 @@ const OlmDevice = require("./OlmDevice");
|
||||
const olmlib = require("./olmlib");
|
||||
const algorithms = require("./algorithms");
|
||||
const DeviceInfo = require("./deviceinfo");
|
||||
import SskInfo from './sskinfo';
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
import { randomString } from '../randomstring';
|
||||
@@ -102,6 +103,8 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
*/
|
||||
export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
clientStore, cryptoStore, roomList, verificationMethods) {
|
||||
this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this);
|
||||
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
@@ -140,6 +143,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._deviceList = new DeviceList(
|
||||
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
|
||||
// server.
|
||||
@@ -255,6 +261,59 @@ Crypto.prototype.init = async function() {
|
||||
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
|
||||
* if one is present and has a valid signature from
|
||||
@@ -719,6 +778,16 @@ Crypto.prototype.saveDeviceList = function(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
|
||||
*
|
||||
|
||||
76
src/crypto/sskinfo.js
Normal file
76
src/crypto/sskinfo.js
Normal 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
|
||||
* <key type>:<id> -> <base64-encoded key>>
|
||||
*
|
||||
* @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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user