From fa26454e44d34f4589adea80af855ea30caa677b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 13 Feb 2023 11:27:28 +0000 Subject: [PATCH 1/7] v2 of MSC3903 implementation This is a deliberate breaking change on an unstable feature. --- spec/unit/rendezvous/ecdh.spec.ts | 26 +++++------ ...l.ts => MSC3903ECDHv2RendezvousChannel.ts} | 46 +++++++++---------- src/rendezvous/channels/index.ts | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) rename src/rendezvous/channels/{MSC3903ECDHv1RendezvousChannel.ts => MSC3903ECDHv2RendezvousChannel.ts} (84%) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index f088977f056..92559bf050e 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import "../../olm-loader"; import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels"; import { decodeBase64 } from "../../../src/crypto/olmlib"; import { DummyTransport } from "./DummyTransport"; @@ -24,7 +24,7 @@ function makeTransport(name: string) { return new DummyTransport(name, { type: "dummy" }); } -describe("ECDHv1", function () { +describe("ECDHv2", function () { beforeAll(async function () { await global.Olm.init(); }); @@ -37,9 +37,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -62,9 +62,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -87,9 +87,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -109,9 +109,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -135,9 +135,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -159,7 +159,7 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); await bobTransport.send({ iv: "dummy", ciphertext: "dummy" }); diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts similarity index 84% rename from src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts rename to src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts index 24ebcbe4c2e..cd1ac47be8b 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +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. @@ -25,20 +25,20 @@ import { RendezvousTransport, RendezvousFailureReason, } from ".."; -import { encodeBase64, decodeBase64 } from "../../crypto/olmlib"; +import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib"; import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; import { generateDecimalSas } from "../../crypto/verification/SASDecimal"; import { UnstableValue } from "../../NamespacedValue"; -const ECDH_V1 = new UnstableValue( - "m.rendezvous.v1.curve25519-aes-sha256", - "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256", +const ECDH = new UnstableValue( + "m.rendezvous.v2.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256", ); -export interface ECDHv1RendezvousCode extends RendezvousCode { +export interface ECDHv2RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; + algorithm: typeof ECDH.name | typeof ECDH.altName; key: string; }; } @@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; export interface PlainTextPayload { - algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; + algorithm: typeof ECDH.name | typeof ECDH.altName; key?: string; } @@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise { * X25519/ECDH key agreement based secure rendezvous channel. * Note that this is UNSTABLE and may have breaking changes without notice. */ -export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { +export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey; @@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); } - public async generateCode(intent: RendezvousIntent): Promise { + public async generateCode(intent: RendezvousIntent): Promise { if (this.transport.ready) { throw new Error("Code already generated"); } - await this.transport.send({ algorithm: ECDH_V1.name }); + await this.transport.send({ algorithm: ECDH.name }); - const rendezvous: ECDHv1RendezvousCode = { + const rendezvous: ECDHv2RendezvousCode = { rendezvous: { - algorithm: ECDH_V1.name, - key: encodeBase64(this.ourPublicKey), + algorithm: ECDH.name, + key: encodeUnpaddedBase64(this.ourPublicKey), transport: await this.transport.details(), }, intent, @@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const res = rawRes as Partial; const { key, algorithm } = res; - if (!algorithm || !ECDH_V1.matches(algorithm) || !key) { + if (!algorithm || !ECDH.matches(algorithm) || !key) { throw new RendezvousError( "Unsupported algorithm: " + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, @@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } else { // send our public key unencrypted await this.transport.send({ - algorithm: ECDH_V1.name, - key: encodeBase64(this.ourPublicKey), + algorithm: ECDH.name, + key: encodeUnpaddedBase64(this.ourPublicKey), }); } this.connected = true; - this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); + this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!)); const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = ECDH_V1.name; - aesInfo += `|${encodeBase64(initiatorKey)}`; - aesInfo += `|${encodeBase64(recipientKey)}`; + let aesInfo = ECDH.name; + aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; + aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`; const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); @@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { ); return { - iv: encodeBase64(iv), - ciphertext: encodeBase64(ciphertext), + iv: encodeUnpaddedBase64(iv), + ciphertext: encodeUnpaddedBase64(ciphertext), }; } diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index 072a1014694..f157bbeaef1 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from "./MSC3903ECDHv1RendezvousChannel"; +export * from "./MSC3903ECDHv2RendezvousChannel"; From 0c24d65e7293fc3588a82ace1d0a4fd46b357710 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 14 Feb 2023 18:26:18 +0000 Subject: [PATCH 2/7] Test correct protocol version --- spec/unit/rendezvous/rendezvous.spec.ts | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 07187647630..73b4c9e92f4 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request"; import "../../olm-loader"; import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; import { - ECDHv1RendezvousCode, + ECDHv2RendezvousCode as ECDHRendezvousCode, MSC3903ECDHPayload, - MSC3903ECDHv1RendezvousChannel, + MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel, } from "../../../src/rendezvous/channels"; import { MatrixClient } from "../../../src"; import { @@ -126,7 +126,7 @@ describe("Rendezvous", function () { fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); expect(aliceRz.code).toBeUndefined(); @@ -181,11 +181,11 @@ describe("Rendezvous", function () { msc3882Enabled: false, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -193,7 +193,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -235,11 +235,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -247,7 +247,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -293,11 +293,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -305,7 +305,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -351,11 +351,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -363,7 +363,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -411,11 +411,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -423,7 +423,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -485,11 +485,11 @@ describe("Rendezvous", function () { master: "mmmmm", }, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -497,7 +497,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, From 23b5cd0e7826ef93df72eba962cfb2e38c649a7e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 14 Feb 2023 18:31:38 +0000 Subject: [PATCH 3/7] Fix up test --- spec/unit/rendezvous/rendezvous.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 73b4c9e92f4..59a3ac71613 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -143,7 +143,7 @@ describe("Rendezvous", function () { const code = JSON.parse(aliceRz.code!) as RendezvousCode; expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); - expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256"); expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1"); expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual( "https://fallbackserver/rz/123", From 61873c2be3efc7eb592afc4117ed1cd9270de3e2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 13 Feb 2023 11:27:28 +0000 Subject: [PATCH 4/7] v2 of MSC3903 implementation This is a deliberate breaking change on an unstable feature. --- spec/unit/rendezvous/ecdh.spec.ts | 26 +++++------ ...l.ts => MSC3903ECDHv2RendezvousChannel.ts} | 46 +++++++++---------- src/rendezvous/channels/index.ts | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) rename src/rendezvous/channels/{MSC3903ECDHv1RendezvousChannel.ts => MSC3903ECDHv2RendezvousChannel.ts} (84%) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index f088977f056..92559bf050e 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import "../../olm-loader"; import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels"; import { decodeBase64 } from "../../../src/crypto/olmlib"; import { DummyTransport } from "./DummyTransport"; @@ -24,7 +24,7 @@ function makeTransport(name: string) { return new DummyTransport(name, { type: "dummy" }); } -describe("ECDHv1", function () { +describe("ECDHv2", function () { beforeAll(async function () { await global.Olm.init(); }); @@ -37,9 +37,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -62,9 +62,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -87,9 +87,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -109,9 +109,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -135,9 +135,9 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -159,7 +159,7 @@ describe("ECDHv1", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); await bobTransport.send({ iv: "dummy", ciphertext: "dummy" }); diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts similarity index 84% rename from src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts rename to src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts index 24ebcbe4c2e..cd1ac47be8b 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +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. @@ -25,20 +25,20 @@ import { RendezvousTransport, RendezvousFailureReason, } from ".."; -import { encodeBase64, decodeBase64 } from "../../crypto/olmlib"; +import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib"; import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; import { generateDecimalSas } from "../../crypto/verification/SASDecimal"; import { UnstableValue } from "../../NamespacedValue"; -const ECDH_V1 = new UnstableValue( - "m.rendezvous.v1.curve25519-aes-sha256", - "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256", +const ECDH = new UnstableValue( + "m.rendezvous.v2.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256", ); -export interface ECDHv1RendezvousCode extends RendezvousCode { +export interface ECDHv2RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; + algorithm: typeof ECDH.name | typeof ECDH.altName; key: string; }; } @@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; export interface PlainTextPayload { - algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; + algorithm: typeof ECDH.name | typeof ECDH.altName; key?: string; } @@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise { * X25519/ECDH key agreement based secure rendezvous channel. * Note that this is UNSTABLE and may have breaking changes without notice. */ -export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { +export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey; @@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); } - public async generateCode(intent: RendezvousIntent): Promise { + public async generateCode(intent: RendezvousIntent): Promise { if (this.transport.ready) { throw new Error("Code already generated"); } - await this.transport.send({ algorithm: ECDH_V1.name }); + await this.transport.send({ algorithm: ECDH.name }); - const rendezvous: ECDHv1RendezvousCode = { + const rendezvous: ECDHv2RendezvousCode = { rendezvous: { - algorithm: ECDH_V1.name, - key: encodeBase64(this.ourPublicKey), + algorithm: ECDH.name, + key: encodeUnpaddedBase64(this.ourPublicKey), transport: await this.transport.details(), }, intent, @@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const res = rawRes as Partial; const { key, algorithm } = res; - if (!algorithm || !ECDH_V1.matches(algorithm) || !key) { + if (!algorithm || !ECDH.matches(algorithm) || !key) { throw new RendezvousError( "Unsupported algorithm: " + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, @@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } else { // send our public key unencrypted await this.transport.send({ - algorithm: ECDH_V1.name, - key: encodeBase64(this.ourPublicKey), + algorithm: ECDH.name, + key: encodeUnpaddedBase64(this.ourPublicKey), }); } this.connected = true; - this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); + this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!)); const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = ECDH_V1.name; - aesInfo += `|${encodeBase64(initiatorKey)}`; - aesInfo += `|${encodeBase64(recipientKey)}`; + let aesInfo = ECDH.name; + aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; + aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`; const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); @@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { ); return { - iv: encodeBase64(iv), - ciphertext: encodeBase64(ciphertext), + iv: encodeUnpaddedBase64(iv), + ciphertext: encodeUnpaddedBase64(ciphertext), }; } diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index 072a1014694..f157bbeaef1 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from "./MSC3903ECDHv1RendezvousChannel"; +export * from "./MSC3903ECDHv2RendezvousChannel"; From 381c918ce606590939d7ebc60aeb63044d1d82bd Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 14 Feb 2023 18:26:18 +0000 Subject: [PATCH 5/7] Test correct protocol version --- spec/unit/rendezvous/rendezvous.spec.ts | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 07187647630..73b4c9e92f4 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request"; import "../../olm-loader"; import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; import { - ECDHv1RendezvousCode, + ECDHv2RendezvousCode as ECDHRendezvousCode, MSC3903ECDHPayload, - MSC3903ECDHv1RendezvousChannel, + MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel, } from "../../../src/rendezvous/channels"; import { MatrixClient } from "../../../src"; import { @@ -126,7 +126,7 @@ describe("Rendezvous", function () { fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); expect(aliceRz.code).toBeUndefined(); @@ -181,11 +181,11 @@ describe("Rendezvous", function () { msc3882Enabled: false, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -193,7 +193,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -235,11 +235,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -247,7 +247,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -293,11 +293,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -305,7 +305,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -351,11 +351,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -363,7 +363,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -411,11 +411,11 @@ describe("Rendezvous", function () { msc3882Enabled: true, msc3886Enabled: false, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -423,7 +423,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, @@ -485,11 +485,11 @@ describe("Rendezvous", function () { master: "mmmmm", }, }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); aliceTransport.onCancelled = aliceOnFailure; await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; expect(code.rendezvous.key).toBeDefined(); @@ -497,7 +497,7 @@ describe("Rendezvous", function () { // bob is try to sign in and scans the code const bobOnFailure = jest.fn(); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + const bobEcdh = new MSC3903ECDHRendezvousChannel( bobTransport, decodeBase64(code.rendezvous.key), // alice's public key bobOnFailure, From 09aa549a0ab5ae89e0cde57b5c0159ff60ff88df Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 14 Feb 2023 18:31:38 +0000 Subject: [PATCH 6/7] Fix up test --- spec/unit/rendezvous/rendezvous.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 73b4c9e92f4..59a3ac71613 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -143,7 +143,7 @@ describe("Rendezvous", function () { const code = JSON.parse(aliceRz.code!) as RendezvousCode; expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); - expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256"); expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1"); expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual( "https://fallbackserver/rz/123", From 89773458b9a1e5f332938e5574f35b16d204d75d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 2 Mar 2023 11:47:55 +0000 Subject: [PATCH 7/7] Reinstate v1 support to make this a non-breaking change Deprecates several experimental types --- spec/unit/rendezvous/ecdhv1.spec.ts | 172 ++++++++++++ .../{ecdh.spec.ts => ecdhv2.spec.ts} | 0 .../MSC3903ECDHv1RendezvousChannel.ts | 256 ++++++++++++++++++ .../MSC3903ECDHv2RendezvousChannel.ts | 17 +- src/rendezvous/channels/index.ts | 1 + 5 files changed, 438 insertions(+), 8 deletions(-) create mode 100644 spec/unit/rendezvous/ecdhv1.spec.ts rename spec/unit/rendezvous/{ecdh.spec.ts => ecdhv2.spec.ts} (100%) create mode 100644 src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts diff --git a/spec/unit/rendezvous/ecdhv1.spec.ts b/spec/unit/rendezvous/ecdhv1.spec.ts new file mode 100644 index 00000000000..f088977f056 --- /dev/null +++ b/spec/unit/rendezvous/ecdhv1.spec.ts @@ -0,0 +1,172 @@ +/* +Copyright 2022 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 "../../olm-loader"; +import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; +import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { decodeBase64 } from "../../../src/crypto/olmlib"; +import { DummyTransport } from "./DummyTransport"; + +function makeTransport(name: string) { + return new DummyTransport(name, { type: "dummy" }); +} + +describe("ECDHv1", function () { + beforeAll(async function () { + await global.Olm.init(); + }); + + describe("with crypto", () => { + it("initiator wants to sign in", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = { key: "xxx" }; + await alice.send(message); + const bobReceive = await bob.receive(); + expect(bobReceive).toEqual(message); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("initiator wants to reciprocate", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = { key: "xxx" }; + await bob.send(message); + const aliceReceive = await alice.receive(); + expect(aliceReceive).toEqual(message); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("double connect", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + expect(alice.connect()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("closed", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + alice.close(); + + expect(alice.connect()).rejects.toThrow(); + expect(alice.send({})).rejects.toThrow(); + expect(alice.receive()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("require ciphertext", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + // send a message without encryption + await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" }); + expect(bob.receive()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("ciphertext before set up", async function () { + const aliceTransport = makeTransport("Alice"); + const bobTransport = makeTransport("Bob"); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + + await bobTransport.send({ iv: "dummy", ciphertext: "dummy" }); + + expect(alice.receive()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + }); + }); +}); diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdhv2.spec.ts similarity index 100% rename from spec/unit/rendezvous/ecdh.spec.ts rename to spec/unit/rendezvous/ecdhv2.spec.ts diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts new file mode 100644 index 00000000000..573ae6f2297 --- /dev/null +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -0,0 +1,256 @@ +/* +Copyright 2022 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 { SAS } from "@matrix-org/olm"; + +import { + RendezvousError, + RendezvousCode, + RendezvousIntent, + RendezvousChannel, + RendezvousTransportDetails, + RendezvousTransport, + RendezvousFailureReason, +} from ".."; +import { encodeBase64, decodeBase64 } from "../../crypto/olmlib"; +import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; +import { generateDecimalSas } from "../../crypto/verification/SASDecimal"; +import { UnstableValue } from "../../NamespacedValue"; +import { EncryptedPayload, MSC3903ECDHPayload, PlainTextPayload } from "./MSC3903ECDHv2RendezvousChannel"; + +/** + * @deprecated Use ECDH_V2 instead + */ +export const ECDH_V1 = new UnstableValue( + "m.rendezvous.v1.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256", +); + +/** + * @deprecated Use ECDHv2RendezvousCode instead + */ +export interface ECDHv1RendezvousCode extends RendezvousCode { + rendezvous: { + transport: RendezvousTransportDetails; + algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; + key: string; + }; +} + +async function importKey(key: Uint8Array): Promise { + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const imported = subtleCrypto.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); + + return imported; +} + +/** + * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) + * X25519/ECDH key agreement based secure rendezvous channel. + * Note that this is UNSTABLE and may have breaking changes without notice. + * + * @deprecated Use MSC3903ECDHv2RendezvousChannel instead. This implementation will be removed. + */ +export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { + private olmSAS?: SAS; + private ourPublicKey: Uint8Array; + private aesKey?: CryptoKey; + private connected = false; + + public constructor( + private transport: RendezvousTransport, + private theirPublicKey?: Uint8Array, + public onFailure?: (reason: RendezvousFailureReason) => void, + ) { + this.olmSAS = new global.Olm.SAS(); + this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); + } + + public async generateCode(intent: RendezvousIntent): Promise { + if (this.transport.ready) { + throw new Error("Code already generated"); + } + + await this.transport.send({ algorithm: ECDH_V1.name }); + + const rendezvous: ECDHv1RendezvousCode = { + rendezvous: { + algorithm: ECDH_V1.name, + key: encodeBase64(this.ourPublicKey), + transport: await this.transport.details(), + }, + intent, + }; + + return rendezvous; + } + + public async connect(): Promise { + if (this.connected) { + throw new Error("Channel already connected"); + } + + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + + const isInitiator = !this.theirPublicKey; + + if (isInitiator) { + // wait for the other side to send us their public key + const rawRes = await this.transport.receive(); + if (!rawRes) { + throw new Error("No response from other device"); + } + const res = rawRes as Partial; + const { key, algorithm } = res; + if (!algorithm || !ECDH_V1.matches(algorithm) || !key) { + throw new RendezvousError( + "Unsupported algorithm: " + algorithm, + RendezvousFailureReason.UnsupportedAlgorithm, + ); + } + + this.theirPublicKey = decodeBase64(key); + } else { + // send our public key unencrypted + await this.transport.send({ + algorithm: ECDH_V1.name, + key: encodeBase64(this.ourPublicKey), + }); + } + + this.connected = true; + + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); + + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; + const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; + let aesInfo = ECDH_V1.name; + aesInfo += `|${encodeBase64(initiatorKey)}`; + aesInfo += `|${encodeBase64(recipientKey)}`; + + const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); + + this.aesKey = await importKey(aesKeyBytes); + + // blank the bytes out to make sure not kept in memory + aesKeyBytes.fill(0); + + const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); + return generateDecimalSas(Array.from(rawChecksum)).join("-"); + } + + private async encrypt(data: T): Promise { + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const iv = new Uint8Array(32); + crypto.getRandomValues(iv); + + const encodedData = new TextEncoder().encode(JSON.stringify(data)); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-GCM", + iv, + tagLength: 128, + }, + this.aesKey as CryptoKey, + encodedData, + ); + + return { + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + }; + } + + public async send(payload: T): Promise { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + + return this.transport.send(await this.encrypt(payload)); + } + + private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise> { + if (!ciphertext || !iv) { + throw new Error("Missing ciphertext and/or iv"); + } + + const ciphertextBytes = decodeBase64(ciphertext); + + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-GCM", + iv: decodeBase64(iv), + tagLength: 128, + }, + this.aesKey as CryptoKey, + ciphertextBytes, + ); + + return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); + } + + public async receive(): Promise | undefined> { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + + const rawData = await this.transport.receive(); + if (!rawData) { + return undefined; + } + const data = rawData as Partial; + if (data.ciphertext && data.iv) { + return this.decrypt(data as EncryptedPayload); + } + + throw new Error("Data received but no ciphertext"); + } + + public async close(): Promise { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + + public async cancel(reason: RendezvousFailureReason): Promise { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} diff --git a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts index cd1ac47be8b..312aaa5c2e3 100644 --- a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -29,8 +29,9 @@ import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib"; import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; import { generateDecimalSas } from "../../crypto/verification/SASDecimal"; import { UnstableValue } from "../../NamespacedValue"; +import { ECDH_V1 } from "./MSC3903ECDHv1RendezvousChannel"; -const ECDH = new UnstableValue( +const ECDH_V2 = new UnstableValue( "m.rendezvous.v2.curve25519-aes-sha256", "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256", ); @@ -38,7 +39,7 @@ const ECDH = new UnstableValue( export interface ECDHv2RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: typeof ECDH.name | typeof ECDH.altName; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName | typeof ECDH_V1.name | typeof ECDH_V1.altName; key: string; }; } @@ -46,7 +47,7 @@ export interface ECDHv2RendezvousCode extends RendezvousCode { export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; export interface PlainTextPayload { - algorithm: typeof ECDH.name | typeof ECDH.altName; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName | typeof ECDH_V1.name | typeof ECDH_V1.altName; key?: string; } @@ -90,11 +91,11 @@ export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { throw new Error("Code already generated"); } - await this.transport.send({ algorithm: ECDH.name }); + await this.transport.send({ algorithm: ECDH_V2.name }); const rendezvous: ECDHv2RendezvousCode = { rendezvous: { - algorithm: ECDH.name, + algorithm: ECDH_V2.name, key: encodeUnpaddedBase64(this.ourPublicKey), transport: await this.transport.details(), }, @@ -123,7 +124,7 @@ export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { } const res = rawRes as Partial; const { key, algorithm } = res; - if (!algorithm || !ECDH.matches(algorithm) || !key) { + if (!algorithm || !ECDH_V2.matches(algorithm) || !key) { throw new RendezvousError( "Unsupported algorithm: " + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, @@ -134,7 +135,7 @@ export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { } else { // send our public key unencrypted await this.transport.send({ - algorithm: ECDH.name, + algorithm: ECDH_V2.name, key: encodeUnpaddedBase64(this.ourPublicKey), }); } @@ -145,7 +146,7 @@ export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = ECDH.name; + let aesInfo = ECDH_V2.name; aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`; diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index f157bbeaef1..42e0ee15e9e 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,4 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ +export * from "./MSC3903ECDHv1RendezvousChannel"; export * from "./MSC3903ECDHv2RendezvousChannel";