This commit is contained in:
RMidhunSuresh 2023-03-01 16:59:24 +05:30
parent b6041cd20c
commit 683e055757
No known key found for this signature in database
10 changed files with 308 additions and 78 deletions

View File

@ -129,6 +129,7 @@ export class CrossSigning {
otherUserId: userId,
platform: this.platform,
deviceMessageHandler: this.deviceMessageHandler,
log
});
return new SASVerification({
room,

View File

@ -13,10 +13,7 @@ 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 {StartVerificationStage} from "./stages/StartVerificationStage";
import {WaitForIncomingMessageStage} from "./stages/WaitForIncomingMessageStage";
import {AcceptVerificationStage} from "./stages/AcceptVerificationStage";
import {SendKeyStage} from "./stages/SendKeyStage";
import {RequestVerificationStage} from "./stages/RequestVerificationStage";
import type {ILogItem} from "../../../logging/types";
import type {Room} from "../../room/Room.js";
import type {Platform} from "../../../platform/web/Platform.js";
@ -48,23 +45,23 @@ export class SASVerification {
// channel.send("m.key.verification.request", {}, log);
try {
const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel };
let stage: BaseSASVerificationStage = new StartVerificationStage(options);
let stage: BaseSASVerificationStage = new RequestVerificationStage(options);
this.startStage = stage;
stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options));
stage = stage.nextStage;
// stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options));
// stage = stage.nextStage;
stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options));
stage = stage.nextStage;
// stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options));
// stage = stage.nextStage;
stage.setNextStage(new AcceptVerificationStage(options));
stage = stage.nextStage;
// stage.setNextStage(new AcceptVerificationStage(options));
// stage = stage.nextStage;
stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options));
stage = stage.nextStage;
// stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options));
// stage = stage.nextStage;
stage.setNextStage(new SendKeyStage(options));
stage = stage.nextStage;
// stage.setNextStage(new SendKeyStage(options));
// stage = stage.nextStage;
console.log("startStage", this.startStage);
}
finally {

View File

@ -20,21 +20,38 @@ import type {ILogItem} from "../../../../logging/types";
import type {Platform} from "../../../../platform/web/Platform.js";
import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js";
import {makeTxnId} from "../../../common.js";
import {CancelTypes, VerificationEventTypes} from "./types";
const messageFromErrorType = {
[CancelTypes.UserCancelled]: "User cancelled this request.",
[CancelTypes.InvalidMessage]: "Invalid Message.",
[CancelTypes.KeyMismatch]: "Key Mismatch.",
[CancelTypes.OtherUserAccepted]: "Another device has accepted this request.",
[CancelTypes.TimedOut]: "Timed Out",
[CancelTypes.UnexpectedMessage]: "Unexpected Message.",
[CancelTypes.UnknownMethod]: "Unknown method.",
[CancelTypes.UnknownTransaction]: "Unknown Transaction.",
[CancelTypes.UserMismatch]: "User Mismatch",
}
const enum ChannelType {
MessageEvent,
ToDeviceMessage,
}
const enum VerificationEventTypes {
Request = "m.key.verification.request",
Ready = "m.key.verification.ready",
}
export interface IChannel {
send(eventType: string, content: any, log: ILogItem): Promise<void>;
waitForEvent(eventType: string): any;
waitForEvent(eventType: string): Promise<any>;
type: ChannelType;
id: string;
sentMessages: Map<string, any>;
receivedMessages: Map<string, any>;
localMessages: Map<string, any>;
setStartMessage(content: any): void;
setInitiatedByUs(value: boolean): void;
initiatedByUs: boolean;
startMessage: any;
cancelVerification(cancellationType: CancelTypes): Promise<void>;
}
type Options = {
@ -43,6 +60,7 @@ type Options = {
otherUserId: string;
platform: Platform;
deviceMessageHandler: DeviceMessageHandler;
log: ILogItem;
}
export class ToDeviceChannel implements IChannel {
@ -51,15 +69,22 @@ export class ToDeviceChannel implements IChannel {
private readonly otherUserId: string;
private readonly platform: Platform;
private readonly deviceMessageHandler: DeviceMessageHandler;
private readonly sentMessages: Map<string, any> = new Map();
private readonly receivedMessages: Map<string, any> = new Map();
public readonly sentMessages: Map<string, any> = new Map();
public readonly receivedMessages: Map<string, any> = new Map();
public readonly localMessages: Map<string, any> = new Map();
private readonly waitMap: Map<string, {resolve: any, promise: Promise<any>}> = new Map();
private readonly log: ILogItem;
private otherUserDeviceId: string;
public startMessage: any;
public id: string;
private _initiatedByUs: boolean;
constructor(options: Options) {
this.hsApi = options.hsApi;
this.deviceTracker = options.deviceTracker;
this.otherUserId = options.otherUserId;
this.platform = options.platform;
this.log = options.log;
this.deviceMessageHandler = options.deviceMessageHandler;
// todo: find a way to dispose this subscription
this.deviceMessageHandler.on("message", ({unencrypted}) => this.handleDeviceMessage(unencrypted))
@ -74,17 +99,28 @@ export class ToDeviceChannel implements IChannel {
if (eventType === VerificationEventTypes.Request) {
// Handle this case specially
await this.handleRequestEventSpecially(eventType, content, log);
this.sentMessages.set(eventType, content);
return;
}
Object.assign(content, { transaction_id: this.id });
const payload = {
messages: {
[this.otherUserId]: {
// check if the following is undefined?
[this.otherUserDeviceId]: content
}
}
}
await this.hsApi.sendToDevice(eventType, payload, this.id, { log }).response();
this.sentMessages.set(eventType, content);
});
}
async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) {
await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => {
const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log);
console.log("devices", devices);
const timestamp = this.platform.clock.now();
const txnId = makeTxnId();
this.id = txnId;
Object.assign(content, { timestamp, transaction_id: txnId });
const payload = {
messages: {
@ -93,14 +129,64 @@ export class ToDeviceChannel implements IChannel {
}
}
}
this.hsApi.sendToDevice(eventType, payload, txnId, { log });
await this.hsApi.sendToDevice(eventType, payload, txnId, { log }).response();
});
}
handleDeviceMessage(event) {
console.log("event", event);
this.resolveAnyWaits(event);
this.receivedMessages.set(event.type, event);
private handleDeviceMessage(event) {
this.log.wrap("ToDeviceChannel.handleDeviceMessage", (log) => {
console.log("event", event);
log.set("event", event);
this.resolveAnyWaits(event);
this.receivedMessages.set(event.type, event);
if (event.type === VerificationEventTypes.Ready) {
this.handleReadyMessage(event, log);
}
});
}
private async handleReadyMessage(event, log: ILogItem) {
try {
const fromDevice = event.content.from_device;
this.otherUserDeviceId = fromDevice;
// We need to send cancel messages to all other devices
const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log);
const otherDevices = devices.filter(device => device.deviceId !== fromDevice);
const cancelMessage = {
code: CancelTypes.OtherUserAccepted,
reason: "An user already accepted this request!",
transaction_id: this.id,
};
const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {});
const payload = {
messages: {
[this.otherUserId]: deviceMessages
}
}
await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response();
}
catch (e) {
console.log(e);
// Do something here
}
}
async cancelVerification(cancellationType: CancelTypes) {
await this.log.wrap("Channel.cancelVerification", async log => {
const payload = {
messages: {
[this.otherUserId]: {
[this.otherUserDeviceId]: {
code: cancellationType,
reason: messageFromErrorType[cancellationType],
transaction_id: this.id,
}
}
}
}
await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response();
});
}
private resolveAnyWaits(event) {
@ -113,15 +199,34 @@ export class ToDeviceChannel implements IChannel {
}
waitForEvent(eventType: string): Promise<any> {
// Check if we already received the message
const receivedMessage = this.receivedMessages.get(eventType);
if (receivedMessage) {
return Promise.resolve(receivedMessage);
}
// Check if we're already waiting for this message
const existingWait = this.waitMap.get(eventType);
if (existingWait) {
return existingWait.promise;
}
let resolve;
// Add to wait map
const promise = new Promise(r => {
resolve = r;
});
this.waitMap.set(eventType, { resolve, promise });
return promise;
}
setStartMessage(event) {
this.startMessage = event;
}
setInitiatedByUs(value: boolean): void {
this._initiatedByUs = value;
}
get initiatedByUs(): boolean {
return this._initiatedByUs;
};
}

View File

@ -0,0 +1,20 @@
export const enum VerificationEventTypes {
Request = "m.key.verification.request",
Ready = "m.key.verification.ready",
Start = "m.key.verification.start",
Accept = "m.key.verification.accept",
Key = "m.key.verification.key",
Cancel = "m.key.verification.cancel",
}
export const enum CancelTypes {
UserCancelled = "m.user",
TimedOut = "m.timeout",
UnknownTransaction = "m.unknown_transaction",
UnknownMethod = "m.unknown_method",
UnexpectedMessage = "m.unexpected_message",
KeyMismatch = "m.key_mismatch",
UserMismatch = "m.user_mismatch",
InvalidMessage = "m.invalid_message",
OtherUserAccepted = "m.accepted",
}

View File

@ -47,9 +47,11 @@ export abstract class BaseSASVerificationStage extends Disposables {
protected previousResult: undefined | any;
protected _nextStage: BaseSASVerificationStage;
protected channel: IChannel;
protected options: Options;
constructor(options: Options) {
super();
this.options = options;
this.room = options.room;
this.ourUser = options.ourUser;
this.otherUserId = options.otherUserId;

View File

@ -15,8 +15,10 @@ limitations under the License.
*/
import {BaseSASVerificationStage} from "./BaseSASVerificationStage";
import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js";
import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage";
import {VerificationEventTypes} from "../channel/types";
export class StartVerificationStage extends BaseSASVerificationStage {
export class RequestVerificationStage extends BaseSASVerificationStage {
async completeStage() {
await this.log.wrap("StartVerificationStage.completeStage", async (log) => {
@ -27,13 +29,14 @@ export class StartVerificationStage extends BaseSASVerificationStage {
// "msgtype": "m.key.verification.request",
// "to": this.otherUserId,
};
const promise = this.trackEventId();
// const promise = this.trackEventId();
// await this.room.sendEvent("m.room.message", content, null, log);
await this.channel.send("m.key.verification.request", content, log);
const c = await this.channel.waitForEvent("m.key.verification.ready");
const eventId = await promise;
console.log("eventId", eventId);
this.setRequestEventId(eventId);
await this.channel.send(VerificationEventTypes.Request, content, log);
this._nextStage = new SelectVerificationMethodStage(this.options);
const readyContent = await this.channel.waitForEvent("m.key.verification.ready");
// const eventId = await promise;
// console.log("eventId", eventId);
// this.setRequestEventId(eventId);
this.dispose();
});
}

View File

@ -0,0 +1,93 @@
/*
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 {BaseSASVerificationStage} from "./BaseSASVerificationStage";
import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants";
import {CancelTypes, VerificationEventTypes} from "../channel/types";
import type {ILogItem} from "../../../../logging/types";
import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage";
export class SelectVerificationMethodStage extends BaseSASVerificationStage {
private hasSentStartMessage = false;
// should somehow emit something that tells the ui to hide the select option
private allowSelection = true;
async completeStage() {
await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => {
const startMessage = this.channel.waitForEvent(VerificationEventTypes.Start);
const acceptMessage = this.channel.waitForEvent(VerificationEventTypes.Accept);
const { content } = await Promise.race([startMessage, acceptMessage]);
if (content.method) {
// We received the start message
this.allowSelection = false;
if (this.hasSentStartMessage) {
await this.resolveStartConflict();
}
else {
this.channel.setStartMessage(this.channel.receivedMessages.get(VerificationEventTypes.Start));
this.channel.setInitiatedByUs(false);
}
}
else {
// We received the accept message
this.channel.setStartMessage(this.channel.sentMessages.get(VerificationEventTypes.Start));
this.channel.setInitiatedByUs(true);
}
if (!this.channel.initiatedByUs) {
// We need to send the accept message next
this.setNextStage(new SendAcceptVerificationStage(this.options));
}
this.dispose();
});
}
async resolveStartConflict() {
const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start);
const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start);
if (receivedStartMessage.content.method !== sentStartMessage.content.method) {
await this.channel.cancelVerification(CancelTypes.UnexpectedMessage);
return;
}
// todo: what happens if we are verifying devices? user-ids would be the same in that case!
// In the case of conflict, the lexicographically smaller id wins
if (this.ourUser.userId < this.otherUserId) {
// use our stat message
this.channel.setStartMessage(sentStartMessage);
this.channel.setInitiatedByUs(true);
}
else {
this.channel.setStartMessage(receivedStartMessage);
this.channel.setInitiatedByUs(false);
}
}
async selectEmojiMethod(log: ILogItem) {
if (!this.allowSelection) { return; }
const content = {
method: "m.sas.v1",
from_device: this.ourUser.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST,
short_authentication_string: SAS_LIST,
};
await this.channel.send(VerificationEventTypes.Start, content, log);
this.hasSentStartMessage = true;
}
get type() {
return "m.key.verification.request";
}
}

View File

@ -15,30 +15,18 @@ limitations under the License.
*/
import {BaseSASVerificationStage} from "./BaseSASVerificationStage";
import anotherjson from "another-json";
// From element-web
type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519";
type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256";
const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST: MacMethod[] = [
"hkdf-hmac-sha256.v2",
"org.matrix.msc3783.hkdf-hmac-sha256",
"hkdf-hmac-sha256",
"hmac-sha256",
];
const SAS_LIST = ["decimal", "emoji"];
const SAS_SET = new Set(SAS_LIST);
export class AcceptVerificationStage extends BaseSASVerificationStage {
import type { KeyAgreement, MacMethod } from "./constants";
import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants";
import { VerificationEventTypes } from "../channel/types";
import { SendKeyStage } from "./SendKeyStage";
export class SendAcceptVerificationStage extends BaseSASVerificationStage {
async completeStage() {
await this.log.wrap("AcceptVerificationStage.completeStage", async (log) => {
const event = this.previousResult["m.key.verification.start"];
await this.log.wrap("SAcceptVerificationStage.completeStage", async (log) => {
const event = this.channel.startMessage;
const content = {
...event.content,
"m.relates_to": event.relation,
// "m.relates_to": event.relation,
};
console.log("content from event", content);
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
@ -63,12 +51,16 @@ export class AcceptVerificationStage extends BaseSASVerificationStage {
rel_type: "m.reference",
}
};
await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log);
this.nextStage?.setResultFromPreviousStage({
...this.previousResult,
[this.type]: contentToSend,
"our_pub_key": ourPubKey,
});
// await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log);
await this.channel.send(VerificationEventTypes.Accept, contentToSend, log);
this.channel.localMessages.set("our_pub_key", ourPubKey);
await this.channel.waitForEvent(VerificationEventTypes.Key);
this._nextStage = new SendKeyStage(this.options);
// this.nextStage?.setResultFromPreviousStage({
// ...this.previousResult,
// [this.type]: contentToSend,
// "our_pub_key": ourPubKey,
// });
this.dispose();
});
}

View File

@ -16,6 +16,7 @@ limitations under the License.
import {BaseSASVerificationStage} from "./BaseSASVerificationStage";
import {generateEmojiSas} from "../generator";
import {ILogItem} from "../../../../lib";
import { VerificationEventTypes } from "../channel/types";
// From element-web
type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519";
@ -41,31 +42,30 @@ type SASUserInfo = {
type SASUserInfoCollection = {
our: SASUserInfo;
their: SASUserInfo;
requestId: string;
id: string;
initiatedByMe: boolean;
};
const calculateKeyAgreement = {
// eslint-disable-next-line @typescript-eslint/naming-convention
"curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array {
console.log("sas.requestId", sas.requestId);
console.log("sas.requestId", sas.id);
const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`;
const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`;
console.log("ourInfo", ourInfo);
console.log("theirInfo", theirInfo);
const initiatedByMe = false;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS|" +
(initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId;
(sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id;
console.log("sasInfo", sasInfo);
return olmSAS.generate_bytes(sasInfo, bytes);
},
"curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array {
const ourInfo = `${sas.our.userId}${sas.our.deviceId}`;
const theirInfo = `${sas.their.userId}${sas.their.deviceId}`;
const initiatedByMe = false;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS" +
(initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId;
(sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id;
return olmSAS.generate_bytes(sasInfo, bytes);
},
} as const;
@ -87,17 +87,18 @@ export class SendKeyStage extends BaseSASVerificationStage {
private async sendKey(key: string, log: ILogItem): Promise<void> {
const contentToSend = {
key,
"m.relates_to": {
event_id: this.requestEventId,
rel_type: "m.reference",
},
// "m.relates_to": {
// event_id: this.requestEventId,
// rel_type: "m.reference",
// },
};
await this.room.sendEvent("m.key.verification.key", contentToSend, null, log);
await this.channel.send(VerificationEventTypes.Key, contentToSend, log);
// await this.room.sendEvent("m.key.verification.key", contentToSend, null, log);
}
private generateSASBytes(): Uint8Array {
const keyAgreement = this.previousResult["m.key.verification.accept"].key_agreement_protocol;
const otherUserDeviceId = this.previousResult["m.key.verification.start"].content.from_device;
const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).key_agreement_protocol;
const otherUserDeviceId = this.channel.startMessage.content.from_device;
const sasBytes = calculateKeyAgreement[keyAgreement]({
our: {
userId: this.ourUser.userId,
@ -109,7 +110,8 @@ export class SendKeyStage extends BaseSASVerificationStage {
deviceId: otherUserDeviceId,
publicKey: this.theirKey,
},
requestId: this.requestEventId,
id: this.channel.id,
initiatedByMe: this.channel.initiatedByUs,
}, this.olmSAS, 6);
return sasBytes;
}
@ -119,7 +121,8 @@ export class SendKeyStage extends BaseSASVerificationStage {
}
get theirKey(): string {
return this.previousResult["m.key.verification.key"].content.key;
const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key);
return content.key;
}
}

View File

@ -0,0 +1,14 @@
// From element-web
export type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519";
export type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256";
export const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
export const HASHES_LIST = ["sha256"];
export const MAC_LIST: MacMethod[] = [
"hkdf-hmac-sha256.v2",
"org.matrix.msc3783.hkdf-hmac-sha256",
"hkdf-hmac-sha256",
"hmac-sha256",
];
export const SAS_LIST = ["decimal", "emoji"];
export const SAS_SET = new Set(SAS_LIST);