diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdhv1.spec.ts similarity index 100% rename from spec/unit/rendezvous/ecdh.spec.ts rename to spec/unit/rendezvous/ecdhv1.spec.ts diff --git a/spec/unit/rendezvous/ecdhv2.spec.ts b/spec/unit/rendezvous/ecdhv2.spec.ts new file mode 100644 index 00000000000..92559bf050e --- /dev/null +++ b/spec/unit/rendezvous/ecdhv2.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, MSC3903ECDHv2RendezvousChannel } 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("ECDHv2", 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 MSC3903ECDHv2RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv2RendezvousChannel(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 MSC3903ECDHv2RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv2RendezvousChannel(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 MSC3903ECDHv2RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv2RendezvousChannel(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 MSC3903ECDHv2RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv2RendezvousChannel(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 MSC3903ECDHv2RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv2RendezvousChannel(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 MSC3903ECDHv2RendezvousChannel(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/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 07187647630..59a3ac71613 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(); @@ -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", @@ -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, diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 24ebcbe4c2e..573ae6f2297 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -29,12 +29,19 @@ 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"; -const ECDH_V1 = new UnstableValue( +/** + * @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; @@ -43,18 +50,6 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { }; } -export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; - -export interface PlainTextPayload { - algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; - key?: string; -} - -export interface EncryptedPayload { - iv: string; - ciphertext: string; -} - async function importKey(key: Uint8Array): Promise { if (!subtleCrypto) { throw new Error("Web Crypto is not available"); @@ -69,6 +64,8 @@ async function importKey(key: Uint8Array): Promise { * 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; diff --git a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts new file mode 100644 index 00000000000..312aaa5c2e3 --- /dev/null +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -0,0 +1,260 @@ +/* +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 { SAS } from "@matrix-org/olm"; + +import { + RendezvousError, + RendezvousCode, + RendezvousIntent, + RendezvousChannel, + RendezvousTransportDetails, + RendezvousTransport, + RendezvousFailureReason, +} from ".."; +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_V2 = new UnstableValue( + "m.rendezvous.v2.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256", +); + +export interface ECDHv2RendezvousCode extends RendezvousCode { + rendezvous: { + transport: RendezvousTransportDetails; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName | typeof ECDH_V1.name | typeof ECDH_V1.altName; + key: string; + }; +} + +export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; + +export interface PlainTextPayload { + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName | typeof ECDH_V1.name | typeof ECDH_V1.altName; + key?: string; +} + +export interface EncryptedPayload { + iv: string; + ciphertext: 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. + */ +export class MSC3903ECDHv2RendezvousChannel 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_V2.name }); + + const rendezvous: ECDHv2RendezvousCode = { + rendezvous: { + algorithm: ECDH_V2.name, + key: encodeUnpaddedBase64(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_V2.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_V2.name, + key: encodeUnpaddedBase64(this.ourPublicKey), + }); + } + + this.connected = true; + + 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_V2.name; + aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; + aesInfo += `|${encodeUnpaddedBase64(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: encodeUnpaddedBase64(iv), + ciphertext: encodeUnpaddedBase64(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/index.ts b/src/rendezvous/channels/index.ts index 072a1014694..42e0ee15e9e 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -15,3 +15,4 @@ limitations under the License. */ export * from "./MSC3903ECDHv1RendezvousChannel"; +export * from "./MSC3903ECDHv2RendezvousChannel";