mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-01-22 10:11:39 +01:00
Merge pull request #1031 from vector-im/cross-signing/verify-msk
Cross-signing: verify MSK with 4S security key
This commit is contained in:
commit
2a6baef259
@ -29,7 +29,12 @@ export class FeaturesViewModel extends ViewModel {
|
||||
name: this.i18n`Audio/video calls`,
|
||||
description: this.i18n`Allows starting and participating in A/V calls compatible with Element Call (MSC3401). Look for the start call option in the room menu ((...) in the right corner) to start a call.`,
|
||||
feature: FeatureFlag.Calls
|
||||
})),
|
||||
})),
|
||||
new FeatureViewModel(this.childOptions({
|
||||
name: this.i18n`Cross-Signing`,
|
||||
description: this.i18n`Allows verifying the identity of people you chat with. This feature is still evolving constantly, expect things to break.`,
|
||||
feature: FeatureFlag.CrossSigning
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,10 @@ export class KeyBackupViewModel extends ViewModel {
|
||||
return this._session.keyBackup.get()?.version;
|
||||
}
|
||||
|
||||
get isMasterKeyTrusted() {
|
||||
return this._session.crossSigning?.isMasterKeyTrusted ?? false;
|
||||
}
|
||||
|
||||
get backupWriteStatus() {
|
||||
const keyBackup = this._session.keyBackup.get();
|
||||
if (!keyBackup) {
|
||||
|
@ -18,6 +18,7 @@ import type {SettingsStorage} from "./platform/web/dom/SettingsStorage";
|
||||
|
||||
export enum FeatureFlag {
|
||||
Calls = 1 << 0,
|
||||
CrossSigning = 1 << 1
|
||||
}
|
||||
|
||||
export class FeatureSet {
|
||||
@ -39,6 +40,10 @@ export class FeatureSet {
|
||||
return this.isFeatureEnabled(FeatureFlag.Calls);
|
||||
}
|
||||
|
||||
get crossSigning(): boolean {
|
||||
return this.isFeatureEnabled(FeatureFlag.CrossSigning);
|
||||
}
|
||||
|
||||
static async load(settingsStorage: SettingsStorage): Promise<FeatureSet> {
|
||||
const flags = await settingsStorage.getInt("enabled_features") || 0;
|
||||
return new FeatureSet(flags);
|
||||
|
@ -31,6 +31,7 @@ import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption";
|
||||
import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption";
|
||||
import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader";
|
||||
import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup";
|
||||
import {CrossSigning} from "./verification/CrossSigning";
|
||||
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
|
||||
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
|
||||
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
|
||||
@ -59,6 +60,7 @@ export class Session {
|
||||
this._storage = storage;
|
||||
this._hsApi = hsApi;
|
||||
this._mediaRepository = mediaRepository;
|
||||
this._features = features;
|
||||
this._syncInfo = null;
|
||||
this._sessionInfo = sessionInfo;
|
||||
this._rooms = new ObservableMap();
|
||||
@ -88,6 +90,7 @@ export class Session {
|
||||
this._getSyncToken = () => this.syncToken;
|
||||
this._olmWorker = olmWorker;
|
||||
this._keyBackup = new ObservableValue(undefined);
|
||||
this._crossSigning = undefined;
|
||||
this._observedRoomStatus = new Map();
|
||||
|
||||
if (olm) {
|
||||
@ -330,6 +333,20 @@ export class Session {
|
||||
txn
|
||||
);
|
||||
if (keyBackup) {
|
||||
if (this._features.crossSigning) {
|
||||
this._crossSigning = new CrossSigning({
|
||||
storage: this._storage,
|
||||
secretStorage,
|
||||
platform: this._platform,
|
||||
olm: this._olm,
|
||||
deviceTracker: this._deviceTracker,
|
||||
hsApi: this._hsApi,
|
||||
ownUserId: this.userId
|
||||
});
|
||||
await log.wrap("enable cross-signing", log => {
|
||||
return this._crossSigning.init(log);
|
||||
});
|
||||
}
|
||||
for (const room of this._rooms.values()) {
|
||||
if (room.isEncrypted) {
|
||||
room.enableKeyBackup(keyBackup);
|
||||
@ -337,6 +354,8 @@ export class Session {
|
||||
}
|
||||
this._keyBackup.set(keyBackup);
|
||||
return true;
|
||||
} else {
|
||||
log.set("no_backup", true);
|
||||
}
|
||||
} catch (err) {
|
||||
log.catch(err);
|
||||
@ -354,6 +373,10 @@ export class Session {
|
||||
return this._keyBackup;
|
||||
}
|
||||
|
||||
get crossSigning() {
|
||||
return this._crossSigning;
|
||||
}
|
||||
|
||||
get hasIdentity() {
|
||||
return !!this._e2eeAccount;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ function createUserIdentity(userId, initialRoomId = undefined) {
|
||||
return {
|
||||
userId: userId,
|
||||
roomIds: initialRoomId ? [initialRoomId] : [],
|
||||
masterKey: undefined,
|
||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED,
|
||||
};
|
||||
}
|
||||
@ -152,6 +153,26 @@ export class DeviceTracker {
|
||||
}
|
||||
}
|
||||
|
||||
async getMasterKeyForUser(userId, hsApi, log) {
|
||||
return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => {
|
||||
let txn = await this._storage.readTxn([
|
||||
this._storage.storeNames.userIdentities
|
||||
]);
|
||||
let userIdentity = await txn.userIdentities.get(userId);
|
||||
if (userIdentity && userIdentity.deviceTrackingStatus !== TRACKING_STATUS_OUTDATED) {
|
||||
return userIdentity.masterKey;
|
||||
}
|
||||
// fetch from hs
|
||||
await this._queryKeys([userId], hsApi, log);
|
||||
// Retreive from storage now
|
||||
txn = await this._storage.readTxn([
|
||||
this._storage.storeNames.userIdentities
|
||||
]);
|
||||
userIdentity = await txn.userIdentities.get(userId);
|
||||
return userIdentity?.masterKey;
|
||||
});
|
||||
}
|
||||
|
||||
async writeHistoryVisibility(room, historyVisibility, syncTxn, log) {
|
||||
const added = [];
|
||||
const removed = [];
|
||||
@ -224,6 +245,7 @@ export class DeviceTracker {
|
||||
"token": this._getSyncToken()
|
||||
}, {log}).response();
|
||||
|
||||
const masterKeys = log.wrap("master keys", log => this._filterValidMasterKeys(deviceKeyResponse, log));
|
||||
const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log));
|
||||
const txn = await this._storage.readWriteTxn([
|
||||
this._storage.storeNames.userIdentities,
|
||||
@ -233,7 +255,7 @@ export class DeviceTracker {
|
||||
try {
|
||||
const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => {
|
||||
const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity);
|
||||
return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn);
|
||||
return await this._storeQueriedDevicesForUserId(userId, masterKeys.get(userId), deviceIdentities, txn);
|
||||
}));
|
||||
deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []);
|
||||
log.set("devices", deviceIdentities.length);
|
||||
@ -245,7 +267,7 @@ export class DeviceTracker {
|
||||
return deviceIdentities;
|
||||
}
|
||||
|
||||
async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) {
|
||||
async _storeQueriedDevicesForUserId(userId, masterKey, deviceIdentities, txn) {
|
||||
const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId);
|
||||
// delete any devices that we know off but are not in the response anymore.
|
||||
// important this happens before checking if the ed25519 key changed,
|
||||
@ -286,11 +308,39 @@ export class DeviceTracker {
|
||||
identity = createUserIdentity(userId);
|
||||
}
|
||||
identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE;
|
||||
identity.masterKey = masterKey;
|
||||
txn.userIdentities.set(identity);
|
||||
|
||||
return allDeviceIdentities;
|
||||
}
|
||||
|
||||
_filterValidMasterKeys(keyQueryResponse, parentLog) {
|
||||
const masterKeys = new Map();
|
||||
const masterKeysResponse = keyQueryResponse["master_keys"];
|
||||
if (!masterKeysResponse) {
|
||||
return masterKeys;
|
||||
}
|
||||
const validMasterKeyResponses = Object.entries(masterKeysResponse).filter(([userId, keyInfo]) => {
|
||||
if (keyInfo["user_id"] !== userId) {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes("master")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => {
|
||||
const keyIds = Object.keys(keyInfo.keys);
|
||||
if (keyIds.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const masterKey = keyInfo.keys[keyIds[0]];
|
||||
msks.set(userId, masterKey);
|
||||
return msks;
|
||||
}, masterKeys);
|
||||
return masterKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<{userId, verifiedKeys: Array<DeviceSection>>}
|
||||
*/
|
||||
@ -631,12 +681,14 @@ export function tests() {
|
||||
const txn = await storage.readTxn([storage.storeNames.userIdentities]);
|
||||
assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), {
|
||||
userId: "@alice:hs.tld",
|
||||
masterKey: undefined,
|
||||
roomIds: [roomId],
|
||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
||||
});
|
||||
assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), {
|
||||
userId: "@bob:hs.tld",
|
||||
roomIds: [roomId],
|
||||
masterKey: undefined,
|
||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
||||
});
|
||||
assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined);
|
||||
|
@ -57,6 +57,7 @@ export class BackupEncryption {
|
||||
encryption.set_recipient_key(pubKey);
|
||||
} catch(err) {
|
||||
decryption.free();
|
||||
encryption.free();
|
||||
throw err;
|
||||
}
|
||||
return new BackupEncryption(encryption, decryption);
|
||||
|
68
src/matrix/verification/CrossSigning.ts
Normal file
68
src/matrix/verification/CrossSigning.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import type {SecretStorage} from "../ssss/SecretStorage";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
import type {Platform} from "../../platform/web/Platform";
|
||||
import type {DeviceTracker} from "../e2ee/DeviceTracker";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
import type {HomeServerApi} from "../net/HomeServerApi";
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
export class CrossSigning {
|
||||
private readonly storage: Storage;
|
||||
private readonly secretStorage: SecretStorage;
|
||||
private readonly platform: Platform;
|
||||
private readonly deviceTracker: DeviceTracker;
|
||||
private readonly olm: Olm;
|
||||
private readonly hsApi: HomeServerApi;
|
||||
private readonly ownUserId: string;
|
||||
private _isMasterKeyTrusted: boolean = false;
|
||||
|
||||
constructor(options: {storage: Storage, secretStorage: SecretStorage, deviceTracker: DeviceTracker, platform: Platform, olm: Olm, ownUserId: string, hsApi: HomeServerApi}) {
|
||||
this.storage = options.storage;
|
||||
this.secretStorage = options.secretStorage;
|
||||
this.platform = options.platform;
|
||||
this.deviceTracker = options.deviceTracker;
|
||||
this.olm = options.olm;
|
||||
this.hsApi = options.hsApi;
|
||||
this.ownUserId = options.ownUserId;
|
||||
}
|
||||
|
||||
async init(log) {
|
||||
// use errorboundary here
|
||||
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);
|
||||
|
||||
const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn);
|
||||
const signing = new this.olm.PkSigning();
|
||||
let derivedPublicKey;
|
||||
try {
|
||||
const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed));
|
||||
derivedPublicKey = signing.init_with_seed(seed);
|
||||
} finally {
|
||||
signing.free();
|
||||
}
|
||||
const publishedMasterKey = await this.deviceTracker.getMasterKeyForUser(this.ownUserId, this.hsApi, log);
|
||||
log.set({publishedMasterKey, derivedPublicKey});
|
||||
this._isMasterKeyTrusted = publishedMasterKey === derivedPublicKey;
|
||||
log.set("isMasterKeyTrusted", this.isMasterKeyTrusted);
|
||||
}
|
||||
|
||||
get isMasterKeyTrusted(): boolean {
|
||||
return this._isMasterKeyTrusted;
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,9 @@ export class KeyBackupSettingsView extends TemplateView {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
t.if(vm => vm.isMasterKeyTrusted, t => {
|
||||
return t.p("Cross-signing master key found and trusted.")
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export class SettingsView extends TemplateView {
|
||||
}, vm.i18n`Log out`)),
|
||||
);
|
||||
settingNodes.push(
|
||||
t.h3("Key backup"),
|
||||
t.h3("Key backup & security"),
|
||||
t.view(new KeyBackupSettingsView(vm.keyBackupViewModel))
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user