Skip to content

Commit

Permalink
Make MSC3906 implementation compatible with Rust Crypto
Browse files Browse the repository at this point in the history
  • Loading branch information
hughns committed Oct 31, 2023
1 parent 1cd8bed commit 2b832db
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 97 deletions.
202 changes: 125 additions & 77 deletions spec/unit/rendezvous/rendezvous.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,75 +23,124 @@ import {
MSC3903ECDHPayload,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { MatrixClient } from "../../../src";
import { Device, MatrixClient } from "../../../src";
import {
MSC3886SimpleHttpRendezvousTransport,
MSC3886SimpleHttpRendezvousTransportDetails,
} from "../../../src/rendezvous/transports";
import { DummyTransport } from "./DummyTransport";
import { decodeBase64 } from "../../../src/base64";
import { logger } from "../../../src/logger";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { CrossSigningKey } from "../../../src/crypto-api";

type UserID = string;
type DeviceID = string;
type Fingerprint = string;
type PartialUserDevices = Map<DeviceID, Partial<Device>>;
type PartialDeviceMap = Map<UserID, PartialUserDevices>;
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;

function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Partial<Device> {
return {
deviceId,
userId,
getFingerprint: () => fingerprint,
};
}

function mockDeviceMap(
userId: UserID,
deviceId: DeviceID,
deviceKey?: Fingerprint,
otherDevices: SimpleDeviceMap = {},
): PartialDeviceMap {
const deviceMap: PartialDeviceMap = new Map();

const myDevices: PartialUserDevices = new Map();
if (deviceKey) {
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
}
deviceMap.set(userId, myDevices);

for (const u in otherDevices) {
let userDevices = deviceMap.get(u);
if (!userDevices) {
userDevices = new Map();
deviceMap.set(u, userDevices);
}
for (const d in otherDevices[u]) {
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
}
}

return deviceMap;
}

function makeMockClient(opts: {
userId: string;
deviceId: string;
deviceKey?: string;
userId: UserID;
deviceId: DeviceID;
deviceKey?: Fingerprint;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
devices?: SimpleDeviceMap;
verificationFunction?: (
userId: string,
deviceId: string,
verified: boolean,
blocked: boolean,
known: boolean,
) => void;
crossSigningIds?: Record<string, string>;
}): MatrixClient {
return {
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
},
getCapabilities() {
return opts.msc3882r0Only
? {}
: {
capabilities: {
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
}): [MatrixClient, PartialDeviceMap] {
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
return [
{
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
},
getCapabilities() {
return opts.msc3882r0Only
? {}
: {
capabilities: {
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
},
};
},
getUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
getDeviceEd25519Key() {
return opts.deviceKey;
},
baseUrl: "https://example.com",
crypto: {
getStoredDevice(userId: string, deviceId: string) {
return opts.devices?.[deviceId] ?? null;
};
},
setDeviceVerification: opts.verificationFunction,
crossSigningInfo: {
getId(key: string) {
return opts.crossSigningIds?.[key];
},
getUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
getDeviceEd25519Key() {
return opts.deviceKey;
},
},
} as unknown as MatrixClient;
baseUrl: "https://example.com",
getCrypto() {
return {
getUserDeviceInfo([userId]: string[], deviceId: string): Promise<PartialDeviceMap> {
return Promise.resolve(deviceMap);
},
getCrossSigningKeyId(key: CrossSigningKey): string | null {
return opts.crossSigningIds?.[key] ?? null;
},
};
},
crypto: {
setDeviceVerification: opts.verificationFunction,
},
} as unknown as MatrixClient,
deviceMap,
];
}

function makeTransport(name: string, uri = "https://test.rz/123456") {
Expand All @@ -106,6 +155,7 @@ describe("Rendezvous", function () {
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
const userId: UserID = "@user:example.com";

beforeEach(function () {
httpBackend = new MockHttpBackend();
Expand All @@ -118,9 +168,9 @@ describe("Rendezvous", function () {
});

it("generate and cancel", async function () {
const alice = makeMockClient({
userId: "@alice:example.com",
deviceId: "DEVICEID",
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled: true,
msc3882r0Only: true,
Expand Down Expand Up @@ -194,8 +244,8 @@ describe("Rendezvous", function () {

// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled,
Expand Down Expand Up @@ -257,8 +307,8 @@ describe("Rendezvous", function () {

// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
Expand Down Expand Up @@ -316,8 +366,8 @@ describe("Rendezvous", function () {

// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
Expand Down Expand Up @@ -375,7 +425,7 @@ describe("Rendezvous", function () {

// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
Expand Down Expand Up @@ -436,7 +486,7 @@ describe("Rendezvous", function () {

// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
Expand Down Expand Up @@ -495,7 +545,7 @@ describe("Rendezvous", function () {
await bobCompleteProm;
});

async function completeLogin(devices: Record<string, Partial<DeviceInfo>>) {
async function completeLogin(devices: SimpleDeviceMap) {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
Expand All @@ -505,8 +555,8 @@ describe("Rendezvous", function () {
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const aliceVerification = jest.fn();
const alice = makeMockClient({
userId: "alice",
const [alice, deviceMap] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
Expand Down Expand Up @@ -575,13 +625,15 @@ describe("Rendezvous", function () {
aliceRz,
bobTransport,
bobEcdh,
devices,
deviceMap,
};
}

it("approve on existing device + verification", async function () {
const { bobEcdh, aliceRz } = await completeLogin({
BOB: {
getFingerprint: () => "bbbb",
[userId]: {
BOB: "bbbb",
},
});
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
Expand All @@ -607,33 +659,29 @@ describe("Rendezvous", function () {
});

it("device appears online within timeout", async function () {
const devices: Record<string, Partial<DeviceInfo>> = {};
const { aliceRz } = await completeLogin(devices);
// device appears after 1 second
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears before the timeout
setTimeout(() => {
devices.BOB = {
getFingerprint: () => "bbbb",
};
deviceMap.get(userId)?.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1000);
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
});

it("device appears online after timeout", async function () {
const devices: Record<string, Partial<DeviceInfo>> = {};
const { aliceRz } = await completeLogin(devices);
// device appears after 1 second
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears after the timeout
setTimeout(() => {
devices.BOB = {
getFingerprint: () => "bbbb",
};
deviceMap.get(userId)?.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1500);
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});

it("mismatched device key", async function () {
const { aliceRz } = await completeLogin({
BOB: {
getFingerprint: () => "XXXX",
[userId]: {
BOB: "XXXX",
},
});
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
Expand Down
Loading

0 comments on commit 2b832db

Please sign in to comment.