Merge pull request #1040 from vector-im/sas-verification

Implement SAS Verification for crosssigning
This commit is contained in:
Bruno Windels 2023-03-30 11:26:43 +02:00 committed by GitHub
commit d8d4f2b61b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2201 additions and 11 deletions

View File

@ -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",

View 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);
}
}

View File

@ -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 {

View File

@ -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 => {

View 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);
},
}
}

View 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!";
}
}

View 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;
};
}

View 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;
}
}

View 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",
}

View 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,
];
}

View 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;
};
}

View File

@ -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>;
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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));
});
}
}

View 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);
});
}
}

View 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));
});
}
}

View 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);
}
}

View 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));
});
}
}

View File

@ -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);
});
}
}

View 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
}
}
}
}

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);

View 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;
}

View File

@ -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;

View File

@ -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"