mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-01-22 18:21: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
@ -30,6 +30,11 @@ export class FeaturesViewModel extends ViewModel {
|
|||||||
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.`,
|
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
|
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;
|
return this._session.keyBackup.get()?.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isMasterKeyTrusted() {
|
||||||
|
return this._session.crossSigning?.isMasterKeyTrusted ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
get backupWriteStatus() {
|
get backupWriteStatus() {
|
||||||
const keyBackup = this._session.keyBackup.get();
|
const keyBackup = this._session.keyBackup.get();
|
||||||
if (!keyBackup) {
|
if (!keyBackup) {
|
||||||
|
@ -18,6 +18,7 @@ import type {SettingsStorage} from "./platform/web/dom/SettingsStorage";
|
|||||||
|
|
||||||
export enum FeatureFlag {
|
export enum FeatureFlag {
|
||||||
Calls = 1 << 0,
|
Calls = 1 << 0,
|
||||||
|
CrossSigning = 1 << 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeatureSet {
|
export class FeatureSet {
|
||||||
@ -39,6 +40,10 @@ export class FeatureSet {
|
|||||||
return this.isFeatureEnabled(FeatureFlag.Calls);
|
return this.isFeatureEnabled(FeatureFlag.Calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get crossSigning(): boolean {
|
||||||
|
return this.isFeatureEnabled(FeatureFlag.CrossSigning);
|
||||||
|
}
|
||||||
|
|
||||||
static async load(settingsStorage: SettingsStorage): Promise<FeatureSet> {
|
static async load(settingsStorage: SettingsStorage): Promise<FeatureSet> {
|
||||||
const flags = await settingsStorage.getInt("enabled_features") || 0;
|
const flags = await settingsStorage.getInt("enabled_features") || 0;
|
||||||
return new FeatureSet(flags);
|
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 {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption";
|
||||||
import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader";
|
import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader";
|
||||||
import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup";
|
import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup";
|
||||||
|
import {CrossSigning} from "./verification/CrossSigning";
|
||||||
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
|
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
|
||||||
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
|
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
|
||||||
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
|
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
|
||||||
@ -59,6 +60,7 @@ export class Session {
|
|||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._hsApi = hsApi;
|
this._hsApi = hsApi;
|
||||||
this._mediaRepository = mediaRepository;
|
this._mediaRepository = mediaRepository;
|
||||||
|
this._features = features;
|
||||||
this._syncInfo = null;
|
this._syncInfo = null;
|
||||||
this._sessionInfo = sessionInfo;
|
this._sessionInfo = sessionInfo;
|
||||||
this._rooms = new ObservableMap();
|
this._rooms = new ObservableMap();
|
||||||
@ -88,6 +90,7 @@ export class Session {
|
|||||||
this._getSyncToken = () => this.syncToken;
|
this._getSyncToken = () => this.syncToken;
|
||||||
this._olmWorker = olmWorker;
|
this._olmWorker = olmWorker;
|
||||||
this._keyBackup = new ObservableValue(undefined);
|
this._keyBackup = new ObservableValue(undefined);
|
||||||
|
this._crossSigning = undefined;
|
||||||
this._observedRoomStatus = new Map();
|
this._observedRoomStatus = new Map();
|
||||||
|
|
||||||
if (olm) {
|
if (olm) {
|
||||||
@ -330,6 +333,20 @@ export class Session {
|
|||||||
txn
|
txn
|
||||||
);
|
);
|
||||||
if (keyBackup) {
|
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()) {
|
for (const room of this._rooms.values()) {
|
||||||
if (room.isEncrypted) {
|
if (room.isEncrypted) {
|
||||||
room.enableKeyBackup(keyBackup);
|
room.enableKeyBackup(keyBackup);
|
||||||
@ -337,6 +354,8 @@ export class Session {
|
|||||||
}
|
}
|
||||||
this._keyBackup.set(keyBackup);
|
this._keyBackup.set(keyBackup);
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
log.set("no_backup", true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.catch(err);
|
log.catch(err);
|
||||||
@ -354,6 +373,10 @@ export class Session {
|
|||||||
return this._keyBackup;
|
return this._keyBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get crossSigning() {
|
||||||
|
return this._crossSigning;
|
||||||
|
}
|
||||||
|
|
||||||
get hasIdentity() {
|
get hasIdentity() {
|
||||||
return !!this._e2eeAccount;
|
return !!this._e2eeAccount;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ function createUserIdentity(userId, initialRoomId = undefined) {
|
|||||||
return {
|
return {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
roomIds: initialRoomId ? [initialRoomId] : [],
|
roomIds: initialRoomId ? [initialRoomId] : [],
|
||||||
|
masterKey: undefined,
|
||||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED,
|
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) {
|
async writeHistoryVisibility(room, historyVisibility, syncTxn, log) {
|
||||||
const added = [];
|
const added = [];
|
||||||
const removed = [];
|
const removed = [];
|
||||||
@ -224,6 +245,7 @@ export class DeviceTracker {
|
|||||||
"token": this._getSyncToken()
|
"token": this._getSyncToken()
|
||||||
}, {log}).response();
|
}, {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 verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log));
|
||||||
const txn = await this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.userIdentities,
|
this._storage.storeNames.userIdentities,
|
||||||
@ -233,7 +255,7 @@ export class DeviceTracker {
|
|||||||
try {
|
try {
|
||||||
const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => {
|
const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => {
|
||||||
const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity);
|
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), []);
|
deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []);
|
||||||
log.set("devices", deviceIdentities.length);
|
log.set("devices", deviceIdentities.length);
|
||||||
@ -245,7 +267,7 @@ export class DeviceTracker {
|
|||||||
return deviceIdentities;
|
return deviceIdentities;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) {
|
async _storeQueriedDevicesForUserId(userId, masterKey, deviceIdentities, txn) {
|
||||||
const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId);
|
const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId);
|
||||||
// delete any devices that we know off but are not in the response anymore.
|
// delete any devices that we know off but are not in the response anymore.
|
||||||
// important this happens before checking if the ed25519 key changed,
|
// important this happens before checking if the ed25519 key changed,
|
||||||
@ -286,11 +308,39 @@ export class DeviceTracker {
|
|||||||
identity = createUserIdentity(userId);
|
identity = createUserIdentity(userId);
|
||||||
}
|
}
|
||||||
identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE;
|
identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE;
|
||||||
|
identity.masterKey = masterKey;
|
||||||
txn.userIdentities.set(identity);
|
txn.userIdentities.set(identity);
|
||||||
|
|
||||||
return allDeviceIdentities;
|
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>>}
|
* @return {Array<{userId, verifiedKeys: Array<DeviceSection>>}
|
||||||
*/
|
*/
|
||||||
@ -631,12 +681,14 @@ export function tests() {
|
|||||||
const txn = await storage.readTxn([storage.storeNames.userIdentities]);
|
const txn = await storage.readTxn([storage.storeNames.userIdentities]);
|
||||||
assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), {
|
assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), {
|
||||||
userId: "@alice:hs.tld",
|
userId: "@alice:hs.tld",
|
||||||
|
masterKey: undefined,
|
||||||
roomIds: [roomId],
|
roomIds: [roomId],
|
||||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
||||||
});
|
});
|
||||||
assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), {
|
assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), {
|
||||||
userId: "@bob:hs.tld",
|
userId: "@bob:hs.tld",
|
||||||
roomIds: [roomId],
|
roomIds: [roomId],
|
||||||
|
masterKey: undefined,
|
||||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
|
||||||
});
|
});
|
||||||
assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined);
|
assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined);
|
||||||
|
@ -57,6 +57,7 @@ export class BackupEncryption {
|
|||||||
encryption.set_recipient_key(pubKey);
|
encryption.set_recipient_key(pubKey);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
decryption.free();
|
decryption.free();
|
||||||
|
encryption.free();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return new BackupEncryption(encryption, decryption);
|
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:
|
default:
|
||||||
return null;
|
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`)),
|
}, vm.i18n`Log out`)),
|
||||||
);
|
);
|
||||||
settingNodes.push(
|
settingNodes.push(
|
||||||
t.h3("Key backup"),
|
t.h3("Key backup & security"),
|
||||||
t.view(new KeyBackupSettingsView(vm.keyBackupViewModel))
|
t.view(new KeyBackupSettingsView(vm.keyBackupViewModel))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user