mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-11-20 03:25:52 +01:00
Merge pull request #1040 from vector-im/sas-verification
Implement SAS Verification for crosssigning
This commit is contained in:
commit
d8d4f2b61b
@ -60,7 +60,7 @@
|
||||
"xxhashjs": "^0.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"another-json": "^0.2.0",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"dompurify": "^2.3.0",
|
||||
|
174
src/fixtures/matrix/sas/events.ts
Normal file
174
src/fixtures/matrix/sas/events.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
POSSIBLE STATES:
|
||||
(following are messages received, not messages sent)
|
||||
ready -> accept -> key -> mac -> done
|
||||
ready -> start -> key -> mac -> done
|
||||
ready -> start -> accept -> key -> mac -> done (when start resolved to use yours)
|
||||
element does not send you request!
|
||||
start -> key -> mac -> done
|
||||
start -> accept -> key -> mac -> done
|
||||
accept -> key -> mac -> done
|
||||
*/
|
||||
|
||||
import {VerificationEventType} from "../../../matrix/verification/SAS/channel/types";
|
||||
|
||||
function generateResponses(userId: string, deviceId: string, txnId: string) {
|
||||
const readyMessage = {
|
||||
content: {
|
||||
methods: ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"],
|
||||
transaction_id: txnId,
|
||||
from_device: deviceId,
|
||||
},
|
||||
type: "m.key.verification.ready",
|
||||
sender: userId,
|
||||
};
|
||||
const startMessage = {
|
||||
content: {
|
||||
method: "m.sas.v1",
|
||||
from_device: deviceId,
|
||||
key_agreement_protocols: ["curve25519-hkdf-sha256", "curve25519"],
|
||||
hashes: ["sha256"],
|
||||
message_authentication_codes: [
|
||||
"hkdf-hmac-sha256.v2",
|
||||
"org.matrix.msc3783.hkdf-hmac-sha256",
|
||||
"hkdf-hmac-sha256",
|
||||
"hmac-sha256",
|
||||
],
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
transaction_id: txnId,
|
||||
},
|
||||
type: "m.key.verification.start",
|
||||
sender: userId,
|
||||
};
|
||||
const acceptMessage = {
|
||||
content: {
|
||||
key_agreement_protocol: "curve25519-hkdf-sha256",
|
||||
hash: "sha256",
|
||||
message_authentication_code: "hkdf-hmac-sha256.v2",
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
commitment: "h2YJESkiXwoGF+i5luu0YmPAKuAsWVeC2VaZOwdzggE",
|
||||
transaction_id: txnId,
|
||||
},
|
||||
type: "m.key.verification.accept",
|
||||
sender: userId,
|
||||
};
|
||||
const keyMessage = {
|
||||
content: {
|
||||
key: "7XA92bSIAq14R69308U80wsJR0K4KAydFG1HtVRYBFA",
|
||||
transaction_id: txnId,
|
||||
},
|
||||
type: "m.key.verification.key",
|
||||
sender: userId,
|
||||
};
|
||||
const macMessage = {
|
||||
content: {
|
||||
mac: {
|
||||
"ed25519:FWKXUYUHTF":
|
||||
"uMOgfISlZTGja2VHmdnK/xe1JNGi7irTzdaVAYSs6Q8",
|
||||
"ed25519:Ot8Y58PueQ7hJVpYWAJkg2qaREJAY/UhGZYOrsd52oo":
|
||||
"SavNqO8PPcAp0+eoLwlU4JWpuMm8GdGuMopPFaS8alY",
|
||||
},
|
||||
keys: "cHnoX3rt9x86RUUb1nyFOa4U/dCJty+EmXCYPeNg6uU",
|
||||
transaction_id: txnId,
|
||||
},
|
||||
type: "m.key.verification.mac",
|
||||
sender: userId,
|
||||
};
|
||||
const doneMessage = {
|
||||
content: {
|
||||
transaction_id: txnId,
|
||||
},
|
||||
type: "m.key.verification.done",
|
||||
sender: userId,
|
||||
};
|
||||
const result = {};
|
||||
for (const message of [readyMessage, startMessage, keyMessage, macMessage, doneMessage, acceptMessage]) {
|
||||
result[message.type] = message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const enum COMBINATIONS {
|
||||
YOU_SENT_REQUEST,
|
||||
YOU_SENT_START,
|
||||
THEY_SENT_START,
|
||||
}
|
||||
|
||||
export class SASFixtures {
|
||||
private order: COMBINATIONS[] = [];
|
||||
private _youWinConflict: boolean = false;
|
||||
|
||||
constructor(private userId: string, private deviceId: string, private txnId: string) { }
|
||||
|
||||
youSentRequest() {
|
||||
this.order.push(COMBINATIONS.YOU_SENT_REQUEST);
|
||||
return this;
|
||||
}
|
||||
|
||||
youSentStart() {
|
||||
this.order.push(COMBINATIONS.YOU_SENT_START);
|
||||
return this;
|
||||
}
|
||||
|
||||
theySentStart() {
|
||||
this.order.push(COMBINATIONS.THEY_SENT_START);
|
||||
return this;
|
||||
}
|
||||
|
||||
youWinConflict() {
|
||||
this._youWinConflict = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
theyWinConflict() {
|
||||
this._youWinConflict = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
fixtures(): Map<VerificationEventType, any> {
|
||||
const responses = generateResponses(this.userId, this.deviceId, this.txnId);
|
||||
const array: any[] = [];
|
||||
const addToArray = (type) => array.push([type, responses[type]]);
|
||||
let i = 0;
|
||||
while(i < this.order.length) {
|
||||
const item = this.order[i];
|
||||
switch (item) {
|
||||
case COMBINATIONS.YOU_SENT_REQUEST:
|
||||
addToArray(VerificationEventType.Ready);
|
||||
break;
|
||||
case COMBINATIONS.THEY_SENT_START: {
|
||||
addToArray(VerificationEventType.Start);
|
||||
const nextItem = this.order[i+1];
|
||||
if (nextItem === COMBINATIONS.YOU_SENT_START) {
|
||||
if (this._youWinConflict) {
|
||||
addToArray(VerificationEventType.Accept);
|
||||
i = i + 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case COMBINATIONS.YOU_SENT_START: {
|
||||
const nextItem = this.order[i+1]
|
||||
if (nextItem === COMBINATIONS.THEY_SENT_START) {
|
||||
if (this._youWinConflict) {
|
||||
addToArray(VerificationEventType.Accept);
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (this.order[i-1] === COMBINATIONS.THEY_SENT_START) {
|
||||
break;
|
||||
}
|
||||
addToArray(VerificationEventType.Accept);
|
||||
break;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
addToArray(VerificationEventType.Key);
|
||||
addToArray(VerificationEventType.Mac);
|
||||
addToArray(VerificationEventType.Done);
|
||||
return new Map(array);
|
||||
}
|
||||
}
|
@ -17,9 +17,11 @@ limitations under the License.
|
||||
import {OLM_ALGORITHM} from "./e2ee/common";
|
||||
import {countBy, groupBy} from "../utils/groupBy";
|
||||
import {LRUCache} from "../utils/LRUCache";
|
||||
import {EventEmitter} from "../utils/EventEmitter";
|
||||
|
||||
export class DeviceMessageHandler {
|
||||
export class DeviceMessageHandler extends EventEmitter{
|
||||
constructor({storage, callHandler}) {
|
||||
super();
|
||||
this._storage = storage;
|
||||
this._olmDecryption = null;
|
||||
this._megolmDecryption = null;
|
||||
@ -39,6 +41,7 @@ export class DeviceMessageHandler {
|
||||
async prepareSync(toDeviceEvents, lock, txn, log) {
|
||||
log.set("messageTypes", countBy(toDeviceEvents, e => e.type));
|
||||
const encryptedEvents = toDeviceEvents.filter(e => e.type === "m.room.encrypted");
|
||||
this._emitUnencryptedEvents(toDeviceEvents);
|
||||
if (!this._olmDecryption) {
|
||||
log.log("can't decrypt, encryption not enabled", log.level.Warn);
|
||||
return;
|
||||
@ -74,6 +77,7 @@ export class DeviceMessageHandler {
|
||||
}
|
||||
|
||||
async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) {
|
||||
this._emitEncryptedEvents(decryptionResults);
|
||||
if (this._callHandler) {
|
||||
// if we don't have a device, we need to fetch the device keys the message claims
|
||||
// and check the keys, and we should only do network requests during
|
||||
@ -101,6 +105,20 @@ export class DeviceMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_emitUnencryptedEvents(toDeviceEvents) {
|
||||
const unencryptedEvents = toDeviceEvents.filter(e => e.type !== "m.room.encrypted");
|
||||
for (const event of unencryptedEvents) {
|
||||
this.emit("message", { unencrypted: event });
|
||||
}
|
||||
}
|
||||
|
||||
_emitEncryptedEvents(decryptionResults) {
|
||||
// We don't emit for now as we're not verifying the identity of the sender
|
||||
// for (const result of decryptionResults) {
|
||||
// this.emit("message", { encrypted: result });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class SyncPreparation {
|
||||
|
@ -14,17 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ILogItem} from "../../logging/types";
|
||||
import {pkSign} from "./common";
|
||||
import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common";
|
||||
|
||||
import {pkSign} from "./common";
|
||||
import {SASVerification} from "./SAS/SASVerification";
|
||||
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
||||
import {VerificationEventType} from "./SAS/channel/types";
|
||||
import type {SecretStorage} from "../ssss/SecretStorage";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
import type {Transaction} from "../storage/idb/Transaction";
|
||||
import type {Platform} from "../../platform/web/Platform";
|
||||
import type {DeviceTracker} from "../e2ee/DeviceTracker";
|
||||
import type {HomeServerApi} from "../net/HomeServerApi";
|
||||
import type {Account} from "../e2ee/Account";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
import type {DeviceMessageHandler} from "../DeviceMessageHandler.js";
|
||||
import type {SignedValue, DeviceKey} from "../e2ee/common";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
type Olm = typeof OlmNamespace;
|
||||
@ -82,7 +84,10 @@ export class CrossSigning {
|
||||
private readonly hsApi: HomeServerApi;
|
||||
private readonly ownUserId: string;
|
||||
private readonly e2eeAccount: Account;
|
||||
private readonly deviceMessageHandler: DeviceMessageHandler;
|
||||
private _isMasterKeyTrusted: boolean = false;
|
||||
private readonly deviceId: string;
|
||||
private sasVerificationInProgress?: SASVerification;
|
||||
|
||||
constructor(options: {
|
||||
storage: Storage,
|
||||
@ -92,8 +97,10 @@ export class CrossSigning {
|
||||
olm: Olm,
|
||||
olmUtil: Olm.Utility,
|
||||
ownUserId: string,
|
||||
deviceId: string,
|
||||
hsApi: HomeServerApi,
|
||||
e2eeAccount: Account
|
||||
e2eeAccount: Account,
|
||||
deviceMessageHandler: DeviceMessageHandler,
|
||||
}) {
|
||||
this.storage = options.storage;
|
||||
this.secretStorage = options.secretStorage;
|
||||
@ -103,7 +110,27 @@ export class CrossSigning {
|
||||
this.olmUtil = options.olmUtil;
|
||||
this.hsApi = options.hsApi;
|
||||
this.ownUserId = options.ownUserId;
|
||||
this.deviceId = options.deviceId;
|
||||
this.e2eeAccount = options.e2eeAccount
|
||||
this.deviceMessageHandler = options.deviceMessageHandler;
|
||||
|
||||
this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => {
|
||||
if (this.sasVerificationInProgress &&
|
||||
(
|
||||
!this.sasVerificationInProgress.finished ||
|
||||
// If the start message is for the previous sasverification, ignore it.
|
||||
this.sasVerificationInProgress.channel.id === unencryptedEvent.content.transaction_id
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (unencryptedEvent.type === VerificationEventType.Request ||
|
||||
unencryptedEvent.type === VerificationEventType.Start) {
|
||||
await this.platform.logger.run("Start verification from request", async (log) => {
|
||||
const sas = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log);
|
||||
await sas?.start();
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @return {boolean} whether cross signing has been enabled on this account */
|
||||
@ -155,6 +182,36 @@ export class CrossSigning {
|
||||
return this._isMasterKeyTrusted;
|
||||
}
|
||||
|
||||
startVerification(userId: string, startingMessage: any, log: ILogItem): SASVerification | undefined {
|
||||
if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) {
|
||||
return;
|
||||
}
|
||||
const channel = new ToDeviceChannel({
|
||||
deviceTracker: this.deviceTracker,
|
||||
hsApi: this.hsApi,
|
||||
otherUserId: userId,
|
||||
clock: this.platform.clock,
|
||||
deviceMessageHandler: this.deviceMessageHandler,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
log
|
||||
}, startingMessage);
|
||||
|
||||
this.sasVerificationInProgress = new SASVerification({
|
||||
olm: this.olm,
|
||||
olmUtil: this.olmUtil,
|
||||
ourUserId: this.ownUserId,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
otherUserId: userId,
|
||||
log,
|
||||
channel,
|
||||
e2eeAccount: this.e2eeAccount,
|
||||
deviceTracker: this.deviceTracker,
|
||||
hsApi: this.hsApi,
|
||||
clock: this.platform.clock,
|
||||
});
|
||||
return this.sasVerificationInProgress;
|
||||
}
|
||||
|
||||
/** returns our own device key signed by our self-signing key. Other signatures will be missing. */
|
||||
async signOwnDevice(log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
return log.wrap("CrossSigning.signOwnDevice", async log => {
|
||||
|
631
src/matrix/verification/SAS/SASVerification.ts
Normal file
631
src/matrix/verification/SAS/SASVerification.ts
Normal file
@ -0,0 +1,631 @@
|
||||
/*
|
||||
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 {SendRequestVerificationStage} from "./stages/SendRequestVerificationStage";
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
import type {BaseSASVerificationStage} from "./stages/BaseSASVerificationStage";
|
||||
import type {Account} from "../../e2ee/Account.js";
|
||||
import type {DeviceTracker} from "../../e2ee/DeviceTracker.js";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
import {IChannel} from "./channel/Channel";
|
||||
import {HomeServerApi} from "../../net/HomeServerApi";
|
||||
import {CancelReason, VerificationEventType} from "./channel/types";
|
||||
import {SendReadyStage} from "./stages/SendReadyStage";
|
||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
import {VerificationCancelledError} from "./VerificationCancelledError";
|
||||
import {Timeout} from "../../../platform/types/types";
|
||||
import {Clock} from "../../../platform/web/dom/Clock.js";
|
||||
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||
import {SASProgressEvents} from "./types";
|
||||
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
type Options = {
|
||||
olm: Olm;
|
||||
olmUtil: Olm.Utility;
|
||||
ourUserId: string;
|
||||
ourUserDeviceId: string;
|
||||
otherUserId: string;
|
||||
channel: IChannel;
|
||||
log: ILogItem;
|
||||
e2eeAccount: Account;
|
||||
deviceTracker: DeviceTracker;
|
||||
hsApi: HomeServerApi;
|
||||
clock: Clock;
|
||||
}
|
||||
|
||||
export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
private startStage: BaseSASVerificationStage;
|
||||
private olmSas: Olm.SAS;
|
||||
public finished: boolean = false;
|
||||
public readonly channel: IChannel;
|
||||
private timeout: Timeout;
|
||||
|
||||
constructor(options: Options) {
|
||||
super();
|
||||
const { olm, channel, clock } = options;
|
||||
const olmSas = new olm.SAS();
|
||||
this.olmSas = olmSas;
|
||||
this.channel = channel;
|
||||
this.setupCancelAfterTimeout(clock);
|
||||
const stageOptions = {...options, olmSas, eventEmitter: this};
|
||||
if (channel.getReceivedMessage(VerificationEventType.Start)) {
|
||||
this.startStage = new SelectVerificationMethodStage(stageOptions);
|
||||
}
|
||||
else if (channel.getReceivedMessage(VerificationEventType.Request)) {
|
||||
this.startStage = new SendReadyStage(stageOptions);
|
||||
}
|
||||
else {
|
||||
this.startStage = new SendRequestVerificationStage(stageOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private async setupCancelAfterTimeout(clock: Clock) {
|
||||
try {
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
this.timeout = clock.createTimeout(tenMinutes);
|
||||
await this.timeout.elapsed();
|
||||
await this.channel.cancelVerification(CancelReason.TimedOut);
|
||||
}
|
||||
catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
let stage = this.startStage;
|
||||
do {
|
||||
await stage.completeStage();
|
||||
stage = stage.nextStage;
|
||||
} while (stage);
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof VerificationCancelledError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.olmSas.free();
|
||||
this.timeout.abort();
|
||||
this.finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import {HomeServer} from "../../../mocks/HomeServer.js";
|
||||
import Olm from "@matrix-org/olm/olm.js";
|
||||
import {MockChannel} from "./channel/MockChannel";
|
||||
import {Clock as MockClock} from "../../../mocks/Clock.js";
|
||||
import {NullLogger} from "../../../logging/NullLogger";
|
||||
import {SASFixtures} from "../../../fixtures/matrix/sas/events";
|
||||
import {SendKeyStage} from "./stages/SendKeyStage";
|
||||
import {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||
import {SendMacStage} from "./stages/SendMacStage";
|
||||
import {VerifyMacStage} from "./stages/VerifyMacStage";
|
||||
import {SendDoneStage} from "./stages/SendDoneStage";
|
||||
import {SendAcceptVerificationStage} from "./stages/SendAcceptVerificationStage";
|
||||
|
||||
export function tests() {
|
||||
|
||||
async function createSASRequest(
|
||||
ourUserId: string,
|
||||
ourDeviceId: string,
|
||||
theirUserId: string,
|
||||
theirDeviceId: string,
|
||||
txnId: string,
|
||||
receivedMessages,
|
||||
startingMessage?: any
|
||||
) {
|
||||
const homeserverMock = new HomeServer();
|
||||
const hsApi = homeserverMock.api;
|
||||
const olm = Olm;
|
||||
await olm.init();
|
||||
const olmUtil = new Olm.Utility();
|
||||
const e2eeAccount = {
|
||||
getDeviceKeysToSignWithCrossSigning: () => {
|
||||
return {
|
||||
keys: {
|
||||
[`ed25519:${ourDeviceId}`]:
|
||||
"srsWWbrnQFIOmUSdrt3cS/unm03qAIgXcWwQg9BegKs",
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const deviceTracker = {
|
||||
getCrossSigningKeyForUser: (userId, __, _hsApi, _) => {
|
||||
let masterKey =
|
||||
userId === ourUserId
|
||||
? "5HIrEawRiiQioViNfezPDWfPWH2pdaw3pbQNHEVN2jM"
|
||||
: "Ot8Y58PueQ7hJVpYWAJkg2qaREJAY/UhGZYOrsd52oo";
|
||||
return {
|
||||
user_id: userId,
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
[`ed25519:${masterKey}`]: masterKey,
|
||||
}
|
||||
};
|
||||
},
|
||||
deviceForId: (_userId, deviceId, _hsApi, _log) => {
|
||||
return {
|
||||
device_id: deviceId,
|
||||
keys: {
|
||||
[`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q",
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
const channel = new MockChannel(
|
||||
theirDeviceId,
|
||||
theirUserId,
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
receivedMessages,
|
||||
deviceTracker,
|
||||
txnId,
|
||||
olm,
|
||||
startingMessage,
|
||||
);
|
||||
const clock = new MockClock();
|
||||
const logger = new NullLogger();
|
||||
return logger.run("log", (log) => {
|
||||
// @ts-ignore
|
||||
const sas = new SASVerification({
|
||||
channel,
|
||||
clock,
|
||||
hsApi,
|
||||
// @ts-ignore
|
||||
deviceTracker,
|
||||
e2eeAccount,
|
||||
olm,
|
||||
olmUtil,
|
||||
otherUserId: theirUserId!,
|
||||
ourUserId,
|
||||
ourUserDeviceId: ourDeviceId,
|
||||
log,
|
||||
});
|
||||
// @ts-ignore
|
||||
channel.setOlmSas(sas.olmSas);
|
||||
sas.on("EmojiGenerated", async (stage) => {
|
||||
await stage?.setEmojiMatch(true);
|
||||
});
|
||||
return { sas, clock, logger };
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"Order of stages created matches expected order when I sent request, they sent start": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.fixtures();
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when I sent request, I sent start": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.youSentStart()
|
||||
.fixtures();
|
||||
const { sas, logger } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
sas.on("SelectVerificationStage", (stage) => {
|
||||
logger.run("send start", async (log) => {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is received": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.theySentStart()
|
||||
.fixtures();
|
||||
const startingMessage = receivedMessages.get(VerificationEventType.Start);
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages,
|
||||
startingMessage,
|
||||
);
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is sent with start conflict (they win)": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.theyWinConflict()
|
||||
.fixtures();
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is sent with start conflict (I win)": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount3:matrix.org";
|
||||
const theirUserId = "@foobaraccount:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.youWinConflict()
|
||||
.fixtures();
|
||||
const { sas, logger } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
sas.on("SelectVerificationStage", (stage) => {
|
||||
logger.run("send start", async (log) => {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is received with start conflict (they win)": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.theyWinConflict()
|
||||
.fixtures();
|
||||
const startingMessage = receivedMessages.get(VerificationEventType.Start);
|
||||
console.log(receivedMessages);
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages,
|
||||
startingMessage,
|
||||
);
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
console.log("Checking", stageClass.constructor.name, stage.constructor.name);
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is received with start conflict (I win)": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount3:matrix.org";
|
||||
const theirUserId = "@foobaraccount:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.youWinConflict()
|
||||
.fixtures();
|
||||
const startingMessage = receivedMessages.get(VerificationEventType.Start);
|
||||
console.log(receivedMessages);
|
||||
const { sas, logger } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages,
|
||||
startingMessage,
|
||||
);
|
||||
sas.on("SelectVerificationStage", (stage) => {
|
||||
logger.run("send start", async (log) => {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
console.log("Checking", stageClass.constructor.name, stage.constructor.name);
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is sent with start conflict (I win), same user": async (assert) => {
|
||||
const ourDeviceId = "FWKXUYUHTF";
|
||||
const ourUserId = "@foobaraccount3:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "ILQHOACESQ";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.youWinConflict()
|
||||
.fixtures();
|
||||
const { sas, logger } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
sas.on("SelectVerificationStage", (stage) => {
|
||||
logger.run("send start", async (log) => {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Order of stages created matches expected order when request is sent with start conflict (they win), same user": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount3:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.youSentStart()
|
||||
.theyWinConflict()
|
||||
.fixtures();
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
SendKeyStage,
|
||||
CalculateSASStage,
|
||||
SendMacStage,
|
||||
VerifyMacStage,
|
||||
SendDoneStage
|
||||
]
|
||||
//@ts-ignore
|
||||
let stage = sas.startStage;
|
||||
for (const stageClass of expectedOrder) {
|
||||
assert.strictEqual(stage instanceof stageClass, true);
|
||||
stage = stage.nextStage;
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Verification is cancelled after 10 minutes": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.fixtures();
|
||||
console.log("receivedMessages", receivedMessages);
|
||||
const { sas, clock } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
const promise = sas.start();
|
||||
clock.elapse(10 * 60 * 1000);
|
||||
try {
|
||||
await promise;
|
||||
}
|
||||
catch (e) {
|
||||
assert.strictEqual(e instanceof VerificationCancelledError, true);
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
"Verification is cancelled when there's no common hash algorithm": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
const ourUserId = "@foobaraccount:matrix.org";
|
||||
const theirUserId = "@foobaraccount3:matrix.org";
|
||||
const theirDeviceId = "FWKXUYUHTF";
|
||||
const txnId = "t150836b91a7bed";
|
||||
const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId)
|
||||
.youSentRequest()
|
||||
.theySentStart()
|
||||
.fixtures();
|
||||
receivedMessages.get(VerificationEventType.Start).content.key_agreement_protocols = ["foo"];
|
||||
const { sas } = await createSASRequest(
|
||||
ourUserId,
|
||||
ourDeviceId,
|
||||
theirUserId,
|
||||
theirDeviceId,
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
try {
|
||||
await sas.start()
|
||||
}
|
||||
catch (e) {
|
||||
assert.strictEqual(e instanceof VerificationCancelledError, true);
|
||||
}
|
||||
assert.strictEqual(sas.finished, true);
|
||||
},
|
||||
}
|
||||
}
|
25
src/matrix/verification/SAS/VerificationCancelledError.ts
Normal file
25
src/matrix/verification/SAS/VerificationCancelledError.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export class VerificationCancelledError extends Error {
|
||||
get name(): string {
|
||||
return "VerificationCancelledError";
|
||||
}
|
||||
|
||||
get message(): string {
|
||||
return "Verification is cancelled!";
|
||||
}
|
||||
}
|
287
src/matrix/verification/SAS/channel/Channel.ts
Normal file
287
src/matrix/verification/SAS/channel/Channel.ts
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
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 {HomeServerApi} from "../../../net/HomeServerApi";
|
||||
import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js";
|
||||
import type {ILogItem} from "../../../../logging/types";
|
||||
import type {Clock} from "../../../../platform/web/dom/Clock.js";
|
||||
import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js";
|
||||
import {makeTxnId} from "../../../common.js";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
import {Disposables} from "../../../../utils/Disposables";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
import {Deferred} from "../../../../utils/Deferred";
|
||||
|
||||
const messageFromErrorType = {
|
||||
[CancelReason.UserCancelled]: "User declined",
|
||||
[CancelReason.InvalidMessage]: "Invalid Message.",
|
||||
[CancelReason.KeyMismatch]: "Key Mismatch.",
|
||||
[CancelReason.OtherDeviceAccepted]: "Another device has accepted this request.",
|
||||
[CancelReason.TimedOut]: "Timed Out",
|
||||
[CancelReason.UnexpectedMessage]: "Unexpected Message.",
|
||||
[CancelReason.UnknownMethod]: "Unknown method.",
|
||||
[CancelReason.UnknownTransaction]: "Unknown Transaction.",
|
||||
[CancelReason.UserMismatch]: "User Mismatch",
|
||||
[CancelReason.MismatchedCommitment]: "Hash commitment does not match.",
|
||||
[CancelReason.MismatchedSAS]: "Emoji/decimal does not match.",
|
||||
}
|
||||
|
||||
export interface IChannel {
|
||||
send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void>;
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any>;
|
||||
getSentMessage(event: VerificationEventType): any;
|
||||
getReceivedMessage(event: VerificationEventType): any;
|
||||
setStartMessage(content: any): void;
|
||||
cancelVerification(cancellationType: CancelReason): Promise<void>;
|
||||
acceptMessage: any;
|
||||
startMessage: any;
|
||||
initiatedByUs: boolean;
|
||||
id: string;
|
||||
otherUserDeviceId: string;
|
||||
}
|
||||
|
||||
type Options = {
|
||||
hsApi: HomeServerApi;
|
||||
deviceTracker: DeviceTracker;
|
||||
otherUserId: string;
|
||||
clock: Clock;
|
||||
deviceMessageHandler: DeviceMessageHandler;
|
||||
log: ILogItem;
|
||||
ourUserDeviceId: string;
|
||||
}
|
||||
|
||||
export class ToDeviceChannel extends Disposables implements IChannel {
|
||||
private readonly hsApi: HomeServerApi;
|
||||
private readonly deviceTracker: DeviceTracker;
|
||||
private ourDeviceId: string;
|
||||
private readonly otherUserId: string;
|
||||
private readonly clock: Clock;
|
||||
private readonly deviceMessageHandler: DeviceMessageHandler;
|
||||
private readonly sentMessages: Map<VerificationEventType, any> = new Map();
|
||||
private readonly receivedMessages: Map<VerificationEventType, any> = new Map();
|
||||
private readonly waitMap: Map<string, Deferred<any>> = new Map();
|
||||
private readonly log: ILogItem;
|
||||
public otherUserDeviceId: string;
|
||||
public startMessage: any;
|
||||
public id: string;
|
||||
private _initiatedByUs: boolean;
|
||||
private _isCancelled = false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param startingMessage Create the channel with existing message in the receivedMessage buffer
|
||||
*/
|
||||
constructor(options: Options, startingMessage?: any) {
|
||||
super();
|
||||
this.hsApi = options.hsApi;
|
||||
this.deviceTracker = options.deviceTracker;
|
||||
this.otherUserId = options.otherUserId;
|
||||
this.ourDeviceId = options.ourUserDeviceId;
|
||||
this.clock = options.clock;
|
||||
this.log = options.log;
|
||||
this.deviceMessageHandler = options.deviceMessageHandler;
|
||||
this.track(
|
||||
this.deviceMessageHandler.disposableOn(
|
||||
"message",
|
||||
async ({ unencrypted }) =>
|
||||
await this.handleDeviceMessage(unencrypted)
|
||||
)
|
||||
);
|
||||
this.track(() => {
|
||||
this.waitMap.forEach((value) => {
|
||||
value.reject(new VerificationCancelledError());
|
||||
});
|
||||
});
|
||||
// Copy over request message
|
||||
if (startingMessage) {
|
||||
/**
|
||||
* startingMessage may be the ready message or the start message.
|
||||
*/
|
||||
this.id = startingMessage.content.transaction_id;
|
||||
this.receivedMessages.set(startingMessage.type, startingMessage);
|
||||
this.otherUserDeviceId = startingMessage.content.from_device;
|
||||
}
|
||||
}
|
||||
|
||||
get isCancelled(): boolean {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void> {
|
||||
await log.wrap("ToDeviceChannel.send", async () => {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
if (eventType === VerificationEventType.Request) {
|
||||
// Handle this case specially
|
||||
await this.handleRequestEventSpecially(eventType, content, log);
|
||||
return;
|
||||
}
|
||||
Object.assign(content, { transaction_id: this.id });
|
||||
const payload = {
|
||||
messages: {
|
||||
[this.otherUserId]: {
|
||||
[this.otherUserDeviceId]: content
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.hsApi.sendToDevice(eventType, payload, makeTxnId(), { log }).response();
|
||||
this.sentMessages.set(eventType, {content});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequestEventSpecially(eventType: VerificationEventType, content: any, log: ILogItem) {
|
||||
await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => {
|
||||
const timestamp = this.clock.now();
|
||||
const txnId = makeTxnId();
|
||||
this.id = txnId;
|
||||
Object.assign(content, { timestamp, transaction_id: txnId });
|
||||
const payload = {
|
||||
messages: {
|
||||
[this.otherUserId]: {
|
||||
"*": content
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.hsApi.sendToDevice(eventType, payload, makeTxnId(), { log }).response();
|
||||
this.sentMessages.set(eventType, {content});
|
||||
});
|
||||
}
|
||||
|
||||
getReceivedMessage(event: VerificationEventType) {
|
||||
return this.receivedMessages.get(event);
|
||||
}
|
||||
|
||||
getSentMessage(event: VerificationEventType) {
|
||||
return this.sentMessages.get(event);
|
||||
}
|
||||
|
||||
get acceptMessage(): any {
|
||||
return this.receivedMessages.get(VerificationEventType.Accept) ??
|
||||
this.sentMessages.get(VerificationEventType.Accept);
|
||||
}
|
||||
|
||||
|
||||
private async handleDeviceMessage(event) {
|
||||
await this.log.wrap("ToDeviceChannel.handleDeviceMessage", async (log) => {
|
||||
if (!event.type.startsWith("m.key.verification.")) {
|
||||
return;
|
||||
}
|
||||
if (event.content.transaction_id !== this.id) {
|
||||
/**
|
||||
* When a device receives an unknown transaction_id, it should send an appropriate
|
||||
* m.key.verification.cancel message to the other device indicating as such.
|
||||
* This does not apply for inbound m.key.verification.start or m.key.verification.cancel messages.
|
||||
*/
|
||||
console.log("Received event with unknown transaction id: ", event);
|
||||
await this.cancelVerification(CancelReason.UnknownTransaction);
|
||||
return;
|
||||
}
|
||||
console.log("event", event);
|
||||
log.log({ l: "event", event });
|
||||
this.resolveAnyWaits(event);
|
||||
this.receivedMessages.set(event.type, event);
|
||||
if (event.type === VerificationEventType.Ready) {
|
||||
this.handleReadyMessage(event, log);
|
||||
return;
|
||||
}
|
||||
if (event.type === VerificationEventType.Cancel) {
|
||||
this._isCancelled = true;
|
||||
this.dispose();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleReadyMessage(event, log: ILogItem) {
|
||||
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.device_id !== fromDevice && device.device_id !== this.ourDeviceId);
|
||||
const cancelMessage = {
|
||||
code: CancelReason.OtherDeviceAccepted,
|
||||
reason: messageFromErrorType[CancelReason.OtherDeviceAccepted],
|
||||
transaction_id: this.id,
|
||||
};
|
||||
const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.device_id] = cancelMessage; return acc; }, {});
|
||||
const payload = {
|
||||
messages: {
|
||||
[this.otherUserId]: deviceMessages
|
||||
}
|
||||
}
|
||||
await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response();
|
||||
}
|
||||
|
||||
async cancelVerification(cancellationType: CancelReason) {
|
||||
await this.log.wrap("Channel.cancelVerification", async log => {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
const payload = {
|
||||
messages: {
|
||||
[this.otherUserId]: {
|
||||
[this.otherUserDeviceId]: {
|
||||
code: cancellationType,
|
||||
reason: messageFromErrorType[cancellationType],
|
||||
transaction_id: this.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response();
|
||||
this._isCancelled = true;
|
||||
this.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private resolveAnyWaits(event) {
|
||||
const { type } = event;
|
||||
const wait = this.waitMap.get(type);
|
||||
if (wait) {
|
||||
wait.resolve(event);
|
||||
this.waitMap.delete(type);
|
||||
}
|
||||
}
|
||||
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any> {
|
||||
if (this._isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
const deferred = new Deferred();
|
||||
this.waitMap.set(eventType, deferred);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
setStartMessage(event) {
|
||||
this.startMessage = event;
|
||||
this._initiatedByUs = event.content.from_device === this.ourDeviceId;
|
||||
}
|
||||
|
||||
get initiatedByUs(): boolean {
|
||||
return this._initiatedByUs;
|
||||
};
|
||||
}
|
137
src/matrix/verification/SAS/channel/MockChannel.ts
Normal file
137
src/matrix/verification/SAS/channel/MockChannel.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import type {ILogItem} from "../../../../lib";
|
||||
import {createCalculateMAC} from "../mac";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
import {IChannel} from "./Channel";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
import {getKeyEd25519Key} from "../../CrossSigning";
|
||||
import {getDeviceEd25519Key} from "../../../e2ee/common";
|
||||
import anotherjson from "another-json";
|
||||
|
||||
interface ITestChannel extends IChannel {
|
||||
setOlmSas(olmSas): void;
|
||||
}
|
||||
|
||||
export class MockChannel implements ITestChannel {
|
||||
public sentMessages: Map<string, any> = new Map();
|
||||
public receivedMessages: Map<string, any> = new Map();
|
||||
public initiatedByUs: boolean;
|
||||
public startMessage: any;
|
||||
public isCancelled: boolean = false;
|
||||
private olmSas: any;
|
||||
|
||||
constructor(
|
||||
public otherUserDeviceId: string,
|
||||
public otherUserId: string,
|
||||
public ourUserId: string,
|
||||
public ourUserDeviceId: string,
|
||||
private fixtures: Map<string, any>,
|
||||
private deviceTracker: any,
|
||||
public id: string,
|
||||
private olm: any,
|
||||
startingMessage?: any,
|
||||
) {
|
||||
if (startingMessage) {
|
||||
const eventType = startingMessage.content.method ? VerificationEventType.Start : VerificationEventType.Request;
|
||||
this.id = startingMessage.content.transaction_id;
|
||||
this.receivedMessages.set(eventType, startingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async send(eventType: string, content: any, _: ILogItem) {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
Object.assign(content, { transaction_id: this.id });
|
||||
this.sentMessages.set(eventType, {content});
|
||||
}
|
||||
|
||||
async waitForEvent(eventType: string): Promise<any> {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
const event = this.fixtures.get(eventType);
|
||||
if (event) {
|
||||
this.receivedMessages.set(eventType, event);
|
||||
}
|
||||
else {
|
||||
await new Promise(() => {});
|
||||
}
|
||||
if (eventType === VerificationEventType.Mac) {
|
||||
await this.recalculateMAC();
|
||||
}
|
||||
if(eventType === VerificationEventType.Accept && this.startMessage) {
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private recalculateCommitment() {
|
||||
const acceptMessage = this.acceptMessage?.content;
|
||||
if (!acceptMessage) {
|
||||
return;
|
||||
}
|
||||
const {content} = this.startMessage;
|
||||
const {content: keyMessage} = this.fixtures.get(VerificationEventType.Key);
|
||||
const key = keyMessage.key;
|
||||
const commitmentStr = key + anotherjson.stringify(content);
|
||||
const olmUtil = new this.olm.Utility();
|
||||
const commitment = olmUtil.sha256(commitmentStr);
|
||||
olmUtil.free();
|
||||
acceptMessage.commitment = commitment;
|
||||
}
|
||||
|
||||
private async recalculateMAC() {
|
||||
// We need to replace the mac with calculated mac
|
||||
const baseInfo =
|
||||
"MATRIX_KEY_VERIFICATION_MAC" +
|
||||
this.otherUserId +
|
||||
this.otherUserDeviceId +
|
||||
this.ourUserId +
|
||||
this.ourUserDeviceId +
|
||||
this.id;
|
||||
const { content: macContent } = this.receivedMessages.get(VerificationEventType.Mac);
|
||||
const macMethod = this.acceptMessage.content.message_authentication_code;
|
||||
const calculateMac = createCalculateMAC(this.olmSas, macMethod);
|
||||
const input = Object.keys(macContent.mac).sort().join(",");
|
||||
const properMac = calculateMac(input, baseInfo + "KEY_IDS");
|
||||
macContent.keys = properMac;
|
||||
for (const keyId of Object.keys(macContent.mac)) {
|
||||
const deviceId = keyId.split(":", 2)[1];
|
||||
const device = await this.deviceTracker.deviceForId(this.otherUserDeviceId, deviceId);
|
||||
if (device) {
|
||||
macContent.mac[keyId] = calculateMac(getDeviceEd25519Key(device), baseInfo + keyId);
|
||||
}
|
||||
else {
|
||||
const key = await this.deviceTracker.getCrossSigningKeyForUser(this.otherUserId);
|
||||
const masterKey = getKeyEd25519Key(key)!;
|
||||
macContent.mac[keyId] = calculateMac(masterKey, baseInfo + keyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStartMessage(event: any): void {
|
||||
this.startMessage = event;
|
||||
this.initiatedByUs = event.content.from_device === this.ourUserDeviceId;
|
||||
this.recalculateCommitment();
|
||||
}
|
||||
|
||||
async cancelVerification(_: CancelReason): Promise<void> {
|
||||
this.isCancelled = true;
|
||||
}
|
||||
|
||||
get acceptMessage(): any {
|
||||
return this.receivedMessages.get(VerificationEventType.Accept) ??
|
||||
this.sentMessages.get(VerificationEventType.Accept);
|
||||
}
|
||||
|
||||
getReceivedMessage(event: VerificationEventType) {
|
||||
return this.receivedMessages.get(event);
|
||||
}
|
||||
|
||||
getSentMessage(event: VerificationEventType) {
|
||||
return this.sentMessages.get(event);
|
||||
}
|
||||
|
||||
setOlmSas(olmSas: any): void {
|
||||
this.olmSas = olmSas;
|
||||
}
|
||||
}
|
25
src/matrix/verification/SAS/channel/types.ts
Normal file
25
src/matrix/verification/SAS/channel/types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const enum VerificationEventType {
|
||||
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",
|
||||
Mac = "m.key.verification.mac",
|
||||
Done = "m.key.verification.done",
|
||||
}
|
||||
|
||||
export const enum CancelReason {
|
||||
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",
|
||||
OtherDeviceAccepted = "m.accepted",
|
||||
// SAS specific
|
||||
MismatchedCommitment = "m.mismatched_commitment",
|
||||
MismatchedSAS = "m.mismatched_sas",
|
||||
}
|
122
src/matrix/verification/SAS/generator.ts
Normal file
122
src/matrix/verification/SAS/generator.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Copied from element-web
|
||||
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
|
||||
const emojiMapping: EmojiMapping[] = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐧", "penguin"], // 10
|
||||
["🐢", "turtle"], // 11
|
||||
["🐟", "fish"], // 12
|
||||
["🐙", "octopus"], // 13
|
||||
["🦋", "butterfly"], // 14
|
||||
["🌷", "flower"], // 15
|
||||
["🌳", "tree"], // 16
|
||||
["🌵", "cactus"], // 17
|
||||
["🍄", "mushroom"], // 18
|
||||
["🌏", "globe"], // 19
|
||||
["🌙", "moon"], // 20
|
||||
["☁️", "cloud"], // 21
|
||||
["🔥", "fire"], // 22
|
||||
["🍌", "banana"], // 23
|
||||
["🍎", "apple"], // 24
|
||||
["🍓", "strawberry"], // 25
|
||||
["🌽", "corn"], // 26
|
||||
["🍕", "pizza"], // 27
|
||||
["🎂", "cake"], // 28
|
||||
["❤️", "heart"], // 29
|
||||
["🙂", "smiley"], // 30
|
||||
["🤖", "robot"], // 31
|
||||
["🎩", "hat"], // 32
|
||||
["👓", "glasses"], // 33
|
||||
["🔧", "spanner"], // 34
|
||||
["🎅", "santa"], // 35
|
||||
["👍", "thumbs up"], // 36
|
||||
["☂️", "umbrella"], // 37
|
||||
["⌛", "hourglass"], // 38
|
||||
["⏰", "clock"], // 39
|
||||
["🎁", "gift"], // 40
|
||||
["💡", "light bulb"], // 41
|
||||
["📕", "book"], // 42
|
||||
["✏️", "pencil"], // 43
|
||||
["📎", "paperclip"], // 44
|
||||
["✂️", "scissors"], // 45
|
||||
["🔒", "lock"], // 46
|
||||
["🔑", "key"], // 47
|
||||
["🔨", "hammer"], // 48
|
||||
["☎️", "telephone"], // 49
|
||||
["🏁", "flag"], // 50
|
||||
["🚂", "train"], // 51
|
||||
["🚲", "bicycle"], // 52
|
||||
["✈️", "aeroplane"], // 53
|
||||
["🚀", "rocket"], // 54
|
||||
["🏆", "trophy"], // 55
|
||||
["⚽", "ball"], // 56
|
||||
["🎸", "guitar"], // 57
|
||||
["🎺", "trumpet"], // 58
|
||||
["🔔", "bell"], // 59
|
||||
["⚓️", "anchor"], // 60
|
||||
["🎧", "headphones"], // 61
|
||||
["📁", "folder"], // 62
|
||||
["📌", "pin"], // 63
|
||||
];
|
||||
|
||||
export function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
|
||||
const emojis = [
|
||||
// just like base64 encoding
|
||||
sasBytes[0] >> 2,
|
||||
((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4),
|
||||
((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6),
|
||||
sasBytes[2] & 0x3f,
|
||||
sasBytes[3] >> 2,
|
||||
((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4),
|
||||
((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6),
|
||||
];
|
||||
return emojis.map((num) => emojiMapping[num]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of decimal encoding of SAS as per:
|
||||
* https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal
|
||||
* @param sasBytes - the five bytes generated by HKDF
|
||||
* @returns the derived three numbers between 1000 and 9191 inclusive
|
||||
*/
|
||||
export function generateDecimalSas(sasBytes: number[]): [number, number, number] {
|
||||
/*
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* bits: 87654321 87654321 87654321 87654321 87654321
|
||||
* \____________/\_____________/\____________/
|
||||
* 1st number 2nd number 3rd number
|
||||
*/
|
||||
return [
|
||||
((sasBytes[0] << 5) | (sasBytes[1] >> 3)) + 1000,
|
||||
(((sasBytes[1] & 0x7) << 10) | (sasBytes[2] << 2) | (sasBytes[3] >> 6)) + 1000,
|
||||
(((sasBytes[3] & 0x3f) << 7) | (sasBytes[4] >> 1)) + 1000,
|
||||
];
|
||||
}
|
30
src/matrix/verification/SAS/mac.ts
Normal file
30
src/matrix/verification/SAS/mac.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 {MacMethod} from "./stages/constants";
|
||||
|
||||
const macMethods: Record<MacMethod, string> = {
|
||||
"hkdf-hmac-sha256": "calculate_mac",
|
||||
"org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
|
||||
"hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64",
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
export function createCalculateMAC(olmSAS: Olm.SAS, method: MacMethod) {
|
||||
return function (input: string, info: string): string {
|
||||
const mac = olmSAS[macMethods[method]](input, info);
|
||||
return mac;
|
||||
};
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
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 {ILogItem} from "../../../../logging/types";
|
||||
import type {Account} from "../../../e2ee/Account.js";
|
||||
import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js";
|
||||
import {IChannel} from "../channel/Channel";
|
||||
import {HomeServerApi} from "../../../net/HomeServerApi";
|
||||
import {SASProgressEvents} from "../types";
|
||||
import {EventEmitter} from "../../../../utils/EventEmitter";
|
||||
|
||||
export type Options = {
|
||||
ourUserId: string;
|
||||
ourUserDeviceId: string;
|
||||
otherUserId: string;
|
||||
log: ILogItem;
|
||||
olmSas: Olm.SAS;
|
||||
olmUtil: Olm.Utility;
|
||||
channel: IChannel;
|
||||
e2eeAccount: Account;
|
||||
deviceTracker: DeviceTracker;
|
||||
hsApi: HomeServerApi;
|
||||
eventEmitter: EventEmitter<SASProgressEvents>
|
||||
}
|
||||
|
||||
export abstract class BaseSASVerificationStage {
|
||||
protected ourUserId: string;
|
||||
protected ourUserDeviceId: string;
|
||||
protected otherUserId: string;
|
||||
protected log: ILogItem;
|
||||
protected olmSAS: Olm.SAS;
|
||||
protected olmUtil: Olm.Utility;
|
||||
protected _nextStage: BaseSASVerificationStage;
|
||||
protected channel: IChannel;
|
||||
protected options: Options;
|
||||
protected e2eeAccount: Account;
|
||||
protected deviceTracker: DeviceTracker;
|
||||
protected hsApi: HomeServerApi;
|
||||
protected eventEmitter: EventEmitter<SASProgressEvents>;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
this.ourUserId = options.ourUserId;
|
||||
this.ourUserDeviceId = options.ourUserDeviceId
|
||||
this.otherUserId = options.otherUserId;
|
||||
this.log = options.log;
|
||||
this.olmSAS = options.olmSas;
|
||||
this.olmUtil = options.olmUtil;
|
||||
this.channel = options.channel;
|
||||
this.e2eeAccount = options.e2eeAccount;
|
||||
this.deviceTracker = options.deviceTracker;
|
||||
this.hsApi = options.hsApi;
|
||||
this.eventEmitter = options.eventEmitter;
|
||||
}
|
||||
|
||||
setNextStage(stage: BaseSASVerificationStage) {
|
||||
this._nextStage = stage;
|
||||
}
|
||||
|
||||
get nextStage(): BaseSASVerificationStage {
|
||||
return this._nextStage;
|
||||
}
|
||||
|
||||
get otherUserDeviceId(): string {
|
||||
const id = this.channel.otherUserDeviceId;
|
||||
if (!id) {
|
||||
throw new Error("Accessed otherUserDeviceId before it was set in channel!");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
abstract completeStage(): Promise<any>;
|
||||
}
|
133
src/matrix/verification/SAS/stages/CalculateSASStage.ts
Normal file
133
src/matrix/verification/SAS/stages/CalculateSASStage.ts
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
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 anotherjson from "another-json";
|
||||
import {BaseSASVerificationStage} from "./BaseSASVerificationStage";
|
||||
import {CancelReason, VerificationEventType} from "../channel/types";
|
||||
import {generateEmojiSas} from "../generator";
|
||||
import {ILogItem} from "../../../../logging/types";
|
||||
import {SendMacStage} from "./SendMacStage";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
|
||||
type SASUserInfo = {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
publicKey: string;
|
||||
};
|
||||
|
||||
type SASUserInfoCollection = {
|
||||
our: SASUserInfo;
|
||||
their: SASUserInfo;
|
||||
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 {
|
||||
const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`;
|
||||
const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS|" +
|
||||
(sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id;
|
||||
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 sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS" +
|
||||
(sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
} as const;
|
||||
|
||||
export class CalculateSASStage extends BaseSASVerificationStage {
|
||||
private resolve: () => void;
|
||||
private reject: (error: VerificationCancelledError) => void;
|
||||
|
||||
public emoji: ReturnType<typeof generateEmojiSas>;
|
||||
|
||||
async completeStage() {
|
||||
await this.log.wrap("CalculateSASStage.completeStage", async (log) => {
|
||||
// 1. Check the hash commitment
|
||||
if (this.channel.initiatedByUs && !await this.verifyHashCommitment(log)) {
|
||||
return;
|
||||
}
|
||||
// 2. Calculate the SAS
|
||||
const emojiConfirmationPromise: Promise<void> = new Promise((res, rej) => {
|
||||
this.resolve = res;
|
||||
this.reject = rej;
|
||||
});
|
||||
this.olmSAS.set_their_key(this.theirKey);
|
||||
const sasBytes = this.generateSASBytes();
|
||||
this.emoji = generateEmojiSas(Array.from(sasBytes));
|
||||
this.eventEmitter.emit("EmojiGenerated", this);
|
||||
await emojiConfirmationPromise;
|
||||
this.setNextStage(new SendMacStage(this.options));
|
||||
});
|
||||
}
|
||||
|
||||
async verifyHashCommitment(log: ILogItem) {
|
||||
return await log.wrap("CalculateSASStage.verifyHashCommitment", async () => {
|
||||
const acceptMessage = this.channel.getReceivedMessage(VerificationEventType.Accept).content;
|
||||
const keyMessage = this.channel.getReceivedMessage(VerificationEventType.Key).content;
|
||||
const commitmentStr = keyMessage.key + anotherjson.stringify(this.channel.startMessage.content);
|
||||
const receivedCommitment = acceptMessage.commitment;
|
||||
const hash = this.olmUtil.sha256(commitmentStr);
|
||||
if (hash !== receivedCommitment) {
|
||||
log.log({l: "Commitment mismatched!", received: receivedCommitment, calculated: hash});
|
||||
await this.channel.cancelVerification(CancelReason.MismatchedCommitment);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private generateSASBytes(): Uint8Array {
|
||||
const keyAgreement = this.channel.acceptMessage.content.key_agreement_protocol;
|
||||
const otherUserDeviceId = this.otherUserDeviceId;
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement]({
|
||||
our: {
|
||||
userId: this.ourUserId,
|
||||
deviceId: this.ourUserDeviceId,
|
||||
publicKey: this.olmSAS.get_pubkey(),
|
||||
},
|
||||
their: {
|
||||
userId: this.otherUserId,
|
||||
deviceId: otherUserDeviceId,
|
||||
publicKey: this.theirKey,
|
||||
},
|
||||
id: this.channel.id,
|
||||
initiatedByMe: this.channel.initiatedByUs,
|
||||
}, this.olmSAS, 6);
|
||||
return sasBytes;
|
||||
}
|
||||
|
||||
async setEmojiMatch(match: boolean) {
|
||||
if (match) {
|
||||
this.resolve();
|
||||
}
|
||||
else {
|
||||
await this.channel.cancelVerification(CancelReason.MismatchedSAS);
|
||||
this.reject(new VerificationCancelledError());
|
||||
}
|
||||
}
|
||||
|
||||
get theirKey(): string {
|
||||
const {content} = this.channel.getReceivedMessage(VerificationEventType.Key);
|
||||
return content.key;
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
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 {CancelReason, VerificationEventType} from "../channel/types";
|
||||
import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants";
|
||||
import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage";
|
||||
import {SendKeyStage} from "./SendKeyStage";
|
||||
import type {ILogItem} from "../../../../logging/types";
|
||||
|
||||
export class SelectVerificationMethodStage extends BaseSASVerificationStage {
|
||||
private hasSentStartMessage = false;
|
||||
private allowSelection = true;
|
||||
|
||||
async completeStage() {
|
||||
await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => {
|
||||
this.eventEmitter.emit("SelectVerificationStage", this);
|
||||
const startMessage = this.channel.waitForEvent(VerificationEventType.Start);
|
||||
const acceptMessage = this.channel.waitForEvent(VerificationEventType.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(log);
|
||||
}
|
||||
else {
|
||||
this.channel.setStartMessage(this.channel.getReceivedMessage(VerificationEventType.Start));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We received the accept message
|
||||
this.channel.setStartMessage(this.channel.getSentMessage(VerificationEventType.Start));
|
||||
}
|
||||
if (this.channel.initiatedByUs) {
|
||||
await acceptMessage;
|
||||
this.setNextStage(new SendKeyStage(this.options));
|
||||
}
|
||||
else {
|
||||
// We need to send the accept message next
|
||||
this.setNextStage(new SendAcceptVerificationStage(this.options));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveStartConflict(log: ILogItem) {
|
||||
await log.wrap("resolveStartConflict", async () => {
|
||||
const receivedStartMessage = this.channel.getReceivedMessage(VerificationEventType.Start);
|
||||
const sentStartMessage = this.channel.getSentMessage(VerificationEventType.Start);
|
||||
if (receivedStartMessage.content.method !== sentStartMessage.content.method) {
|
||||
/**
|
||||
* If the two m.key.verification.start messages do not specify the same verification method,
|
||||
* then the verification should be cancelled with a code of m.unexpected_message.
|
||||
*/
|
||||
log.log({
|
||||
l: "Methods don't match for the start messages",
|
||||
received: receivedStartMessage.content.method,
|
||||
sent: sentStartMessage.content.method,
|
||||
});
|
||||
await this.channel.cancelVerification(CancelReason.UnexpectedMessage);
|
||||
return;
|
||||
}
|
||||
// In the case of conflict, the lexicographically smaller id wins
|
||||
const our = this.ourUserId === this.otherUserId ? this.ourUserDeviceId : this.ourUserId;
|
||||
const their = this.ourUserId === this.otherUserId ? this.otherUserDeviceId : this.otherUserId;
|
||||
const startMessageToUse = our < their ? sentStartMessage : receivedStartMessage;
|
||||
log.log({ l: "Start message resolved", message: startMessageToUse, our, their })
|
||||
this.channel.setStartMessage(startMessageToUse);
|
||||
});
|
||||
}
|
||||
|
||||
async selectEmojiMethod(log: ILogItem) {
|
||||
if (!this.allowSelection) { return; }
|
||||
const content = {
|
||||
method: "m.sas.v1",
|
||||
from_device: this.ourUserDeviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
hashes: HASHES_LIST,
|
||||
message_authentication_codes: MAC_LIST,
|
||||
short_authentication_string: SAS_LIST,
|
||||
};
|
||||
/**
|
||||
* Once we send the start event, we should eventually receive the accept message.
|
||||
* This will cause the Promise.race in completeStage() to resolve and we'll move
|
||||
* to the next stage (where we will send the key).
|
||||
*/
|
||||
await this.channel.send(VerificationEventType.Start, content, log);
|
||||
this.hasSentStartMessage = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 anotherjson from "another-json";
|
||||
import {BaseSASVerificationStage} from "./BaseSASVerificationStage";
|
||||
import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants";
|
||||
import {CancelReason, VerificationEventType} from "../channel/types";
|
||||
import {SendKeyStage} from "./SendKeyStage";
|
||||
|
||||
// from element-web
|
||||
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
|
||||
return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
export class SendAcceptVerificationStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendAcceptVerificationStage.completeStage", async (log) => {
|
||||
const {content: startMessage} = this.channel.startMessage;
|
||||
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(startMessage.key_agreement_protocols))[0];
|
||||
const hashMethod = intersection(HASHES_LIST, new Set(startMessage.hashes))[0];
|
||||
const macMethod = intersection(MAC_LIST, new Set(startMessage.message_authentication_codes))[0];
|
||||
const sasMethod = intersection(startMessage.short_authentication_string, SAS_SET);
|
||||
if (!keyAgreement || !hashMethod || !macMethod || !sasMethod.length) {
|
||||
await this.channel.cancelVerification(CancelReason.UnknownMethod);
|
||||
return;
|
||||
}
|
||||
const ourPubKey = this.olmSAS.get_pubkey();
|
||||
const commitmentStr = ourPubKey + anotherjson.stringify(startMessage);
|
||||
const content = {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
short_authentication_string: sasMethod,
|
||||
commitment: this.olmUtil.sha256(commitmentStr),
|
||||
};
|
||||
await this.channel.send(VerificationEventType.Accept, content, log);
|
||||
await this.channel.waitForEvent(VerificationEventType.Key);
|
||||
this.setNextStage(new SendKeyStage(this.options));
|
||||
});
|
||||
}
|
||||
}
|
25
src/matrix/verification/SAS/stages/SendDoneStage.ts
Normal file
25
src/matrix/verification/SAS/stages/SendDoneStage.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 {VerificationEventType} from "../channel/types";
|
||||
|
||||
export class SendDoneStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendDoneStage.completeStage", async (log) => {
|
||||
await this.channel.send(VerificationEventType.Done, {}, log);
|
||||
});
|
||||
}
|
||||
}
|
35
src/matrix/verification/SAS/stages/SendKeyStage.ts
Normal file
35
src/matrix/verification/SAS/stages/SendKeyStage.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 {VerificationEventType} from "../channel/types";
|
||||
import {CalculateSASStage} from "./CalculateSASStage";
|
||||
|
||||
export class SendKeyStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendKeyStage.completeStage", async (log) => {
|
||||
const ourSasKey = this.olmSAS.get_pubkey();
|
||||
await this.channel.send(VerificationEventType.Key, {key: ourSasKey}, log);
|
||||
/**
|
||||
* We may have already got the key in SendAcceptVerificationStage,
|
||||
* in which case waitForEvent will return a resolved promise with
|
||||
* that content. Otherwise, waitForEvent will actually wait for the
|
||||
* key message.
|
||||
*/
|
||||
await this.channel.waitForEvent(VerificationEventType.Key);
|
||||
this.setNextStage(new CalculateSASStage(this.options));
|
||||
});
|
||||
}
|
||||
}
|
67
src/matrix/verification/SAS/stages/SendMacStage.ts
Normal file
67
src/matrix/verification/SAS/stages/SendMacStage.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 {ILogItem} from "../../../../logging/types";
|
||||
import {VerificationEventType} from "../channel/types";
|
||||
import {createCalculateMAC} from "../mac";
|
||||
import {VerifyMacStage} from "./VerifyMacStage";
|
||||
import {getKeyEd25519Key, KeyUsage} from "../../CrossSigning";
|
||||
|
||||
export class SendMacStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendMacStage.completeStage", async (log) => {
|
||||
const acceptMessage = this.channel.acceptMessage.content;
|
||||
const macMethod = acceptMessage.message_authentication_code;
|
||||
const calculateMAC = createCalculateMAC(this.olmSAS, macMethod);
|
||||
await this.sendMAC(calculateMAC, log);
|
||||
await this.channel.waitForEvent(VerificationEventType.Mac);
|
||||
this.setNextStage(new VerifyMacStage(this.options));
|
||||
});
|
||||
}
|
||||
|
||||
private async sendMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise<void> {
|
||||
const mac: Record<string, string> = {};
|
||||
const keyList: string[] = [];
|
||||
const baseInfo =
|
||||
"MATRIX_KEY_VERIFICATION_MAC" +
|
||||
this.ourUserId +
|
||||
this.ourUserDeviceId +
|
||||
this.otherUserId +
|
||||
this.otherUserDeviceId +
|
||||
this.channel.id;
|
||||
|
||||
const deviceKeyId = `ed25519:${this.ourUserDeviceId}`;
|
||||
const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning();
|
||||
mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId);
|
||||
keyList.push(deviceKeyId);
|
||||
|
||||
const key = await this.deviceTracker.getCrossSigningKeyForUser(this.ourUserId, KeyUsage.Master, this.hsApi, log);
|
||||
if (!key) {
|
||||
log.log({ l: "Fetching msk failed", userId: this.ourUserId });
|
||||
throw new Error("Fetching MSK for user failed!");
|
||||
}
|
||||
const crossSigningKey = getKeyEd25519Key(key);
|
||||
if (crossSigningKey) {
|
||||
const crossSigningKeyId = `ed25519:${crossSigningKey}`;
|
||||
mac[crossSigningKeyId] = calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId);
|
||||
keyList.push(crossSigningKeyId);
|
||||
}
|
||||
|
||||
const keys = calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS");
|
||||
await this.channel.send(VerificationEventType.Mac, { mac, keys }, log);
|
||||
}
|
||||
}
|
||||
|
31
src/matrix/verification/SAS/stages/SendReadyStage.ts
Normal file
31
src/matrix/verification/SAS/stages/SendReadyStage.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 {VerificationEventType} from "../channel/types";
|
||||
import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage";
|
||||
|
||||
export class SendReadyStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendReadyStage.completeStage", async (log) => {
|
||||
const content = {
|
||||
"from_device": this.ourUserDeviceId,
|
||||
"methods": ["m.sas.v1"],
|
||||
};
|
||||
await this.channel.send(VerificationEventType.Ready, content, log);
|
||||
this.setNextStage(new SelectVerificationMethodStage(this.options));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
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 {SelectVerificationMethodStage} from "./SelectVerificationMethodStage";
|
||||
import {VerificationEventType} from "../channel/types";
|
||||
|
||||
export class SendRequestVerificationStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendRequestVerificationStage.completeStage", async (log) => {
|
||||
const content = {
|
||||
"from_device": this.ourUserDeviceId,
|
||||
"methods": ["m.sas.v1"],
|
||||
};
|
||||
await this.channel.send(VerificationEventType.Request, content, log);
|
||||
this.setNextStage(new SelectVerificationMethodStage(this.options));
|
||||
await this.channel.waitForEvent(VerificationEventType.Ready);
|
||||
});
|
||||
}
|
||||
}
|
86
src/matrix/verification/SAS/stages/VerifyMacStage.ts
Normal file
86
src/matrix/verification/SAS/stages/VerifyMacStage.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
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 {ILogItem} from "../../../../logging/types";
|
||||
import {CancelReason, VerificationEventType} from "../channel/types";
|
||||
import {createCalculateMAC} from "../mac";
|
||||
import {SendDoneStage} from "./SendDoneStage";
|
||||
import {KeyUsage, getKeyEd25519Key} from "../../CrossSigning";
|
||||
import {getDeviceEd25519Key} from "../../../e2ee/common";
|
||||
|
||||
export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void;
|
||||
|
||||
export class VerifyMacStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("VerifyMacStage.completeStage", async (log) => {
|
||||
const acceptMessage = this.channel.acceptMessage.content;
|
||||
const macMethod = acceptMessage.message_authentication_code;
|
||||
const calculateMAC = createCalculateMAC(this.olmSAS, macMethod);
|
||||
await this.checkMAC(calculateMAC, log);
|
||||
await this.channel.waitForEvent(VerificationEventType.Done);
|
||||
this.setNextStage(new SendDoneStage(this.options));
|
||||
});
|
||||
}
|
||||
|
||||
private async checkMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise<void> {
|
||||
const {content} = this.channel.getReceivedMessage(VerificationEventType.Mac);
|
||||
const baseInfo =
|
||||
"MATRIX_KEY_VERIFICATION_MAC" +
|
||||
this.otherUserId +
|
||||
this.otherUserDeviceId +
|
||||
this.ourUserId +
|
||||
this.ourUserDeviceId +
|
||||
this.channel.id;
|
||||
|
||||
const calculatedMAC = calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS");
|
||||
if (content.keys !== calculatedMAC) {
|
||||
log.log({ l: "MAC verification failed for keys field", keys: content.keys, calculated: calculatedMAC });
|
||||
this.channel.cancelVerification(CancelReason.KeyMismatch);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.verifyKeys(content.mac, (keyId, key, keyInfo) => {
|
||||
const calculatedMAC = calculateMAC(key, baseInfo + keyId);
|
||||
if (keyInfo !== calculatedMAC) {
|
||||
log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculatedMAC, keyId, key });
|
||||
this.channel.cancelVerification(CancelReason.KeyMismatch);
|
||||
return;
|
||||
}
|
||||
}, log);
|
||||
}
|
||||
|
||||
protected async verifyKeys(keys: Record<string, string>, verifier: KeyVerifier, log: ILogItem): Promise<void> {
|
||||
const userId = this.otherUserId;
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceIdOrMSK = keyId.split(":", 2)[1];
|
||||
const device = await this.deviceTracker.deviceForId(userId, deviceIdOrMSK, this.hsApi, log);
|
||||
if (device) {
|
||||
verifier(keyId, getDeviceEd25519Key(device), keyInfo);
|
||||
// todo: mark device as verified here
|
||||
} else {
|
||||
// If we were not able to find the device, then deviceIdOrMSK is actually the MSK!
|
||||
const key = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log);
|
||||
if (!key) {
|
||||
log.log({ l: "Fetching msk failed", userId });
|
||||
throw new Error("Fetching MSK for user failed!");
|
||||
}
|
||||
const masterKey = getKeyEd25519Key(key);
|
||||
verifier(keyId, masterKey, keyInfo);
|
||||
// todo: mark user as verified here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/matrix/verification/SAS/stages/constants.ts
Normal file
14
src/matrix/verification/SAS/stages/constants.ts
Normal 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);
|
22
src/matrix/verification/SAS/types.ts
Normal file
22
src/matrix/verification/SAS/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
|
||||
export type SASProgressEvents = {
|
||||
SelectVerificationStage: SelectVerificationMethodStage;
|
||||
EmojiGenerated: CalculateSASStage;
|
||||
}
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
export class Deferred<T> {
|
||||
public readonly promise: Promise<T>;
|
||||
public readonly resolve: (value: T) => void;
|
||||
|
@ -52,9 +52,9 @@
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
|
||||
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz":
|
||||
version "3.2.8"
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856"
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"
|
||||
|
||||
"@matrixdotorg/structured-logviewer@^0.0.3":
|
||||
version "0.0.3"
|
||||
|
Loading…
Reference in New Issue
Block a user