From c1c7e51833392b30f4f47b8a7d203e5fbc010f7e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 13 Feb 2023 11:27:28 +0000 Subject: [PATCH 1/3] v2 of MSC3903 implementation This is a deliberate breaking change on an unstable feature. --- spec/unit/rendezvous/ecdhv1.spec.ts | 26 +- .../MSC3903ECDHv1RendezvousChannel.ts | 256 ------------------ .../MSC3903ECDHv2RendezvousChannel.ts | 5 +- src/rendezvous/channels/index.ts | 1 - 4 files changed, 15 insertions(+), 273 deletions(-) delete mode 100644 src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts diff --git a/spec/unit/rendezvous/ecdhv1.spec.ts b/spec/unit/rendezvous/ecdhv1.spec.ts index f088977f056..92559bf050e 100644 --- a/spec/unit/rendezvous/ecdhv1.spec.ts +++ b/spec/unit/rendezvous/ecdhv1.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/MSC3903ECDHv1RendezvousChannel.ts deleted file mode 100644 index 573ae6f2297..00000000000 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* -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 312aaa5c2e3..be60ee5c9aa 100644 --- a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -29,7 +29,6 @@ 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", @@ -39,7 +38,7 @@ const ECDH_V2 = new UnstableValue( 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; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName; key: string; }; } @@ -47,7 +46,7 @@ export interface ECDHv2RendezvousCode extends RendezvousCode { 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; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName; key?: string; } diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index 42e0ee15e9e..f157bbeaef1 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,5 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from "./MSC3903ECDHv1RendezvousChannel"; export * from "./MSC3903ECDHv2RendezvousChannel"; From ca3784e038c507805a4bd0c439419123a1f722bf Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 2 Mar 2023 11:47:55 +0000 Subject: [PATCH 2/3] Reinstate v1 support to make this a non-breaking change Deprecates several experimental types --- spec/unit/rendezvous/ecdhv1.spec.ts | 26 +- .../MSC3903ECDHv1RendezvousChannel.ts | 256 ++++++++++++++++++ .../MSC3903ECDHv2RendezvousChannel.ts | 1 + src/rendezvous/channels/index.ts | 1 + 4 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts diff --git a/spec/unit/rendezvous/ecdhv1.spec.ts b/spec/unit/rendezvous/ecdhv1.spec.ts index 92559bf050e..f088977f056 100644 --- a/spec/unit/rendezvous/ecdhv1.spec.ts +++ b/spec/unit/rendezvous/ecdhv1.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import "../../olm-loader"; import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels"; +import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } 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("ECDHv2", function () { +describe("ECDHv1", function () { beforeAll(async function () { await global.Olm.init(); }); @@ -37,9 +37,9 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -62,9 +62,9 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -87,9 +87,9 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -109,9 +109,9 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -135,9 +135,9 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -159,7 +159,7 @@ describe("ECDHv2", function () { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(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/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 be60ee5c9aa..bb3a1943dba 100644 --- a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -29,6 +29,7 @@ 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", 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"; From db91c0cdef1f8886300a34cd2891152088ee6843 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 2 Mar 2023 12:02:28 +0000 Subject: [PATCH 3/3] Remove MSC3903 v1 support This is a breaking change in code marked unstable/experimental Revert "Reinstate v1 support to make this a non-breaking change" This reverts commit 89773458b9a1e5f332938e5574f35b16d204d75d. --- spec/unit/rendezvous/ecdhv1.spec.ts | 172 ------------ .../MSC3903ECDHv1RendezvousChannel.ts | 256 ------------------ .../MSC3903ECDHv2RendezvousChannel.ts | 1 - src/rendezvous/channels/index.ts | 1 - 4 files changed, 430 deletions(-) delete mode 100644 spec/unit/rendezvous/ecdhv1.spec.ts delete mode 100644 src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts diff --git a/spec/unit/rendezvous/ecdhv1.spec.ts b/spec/unit/rendezvous/ecdhv1.spec.ts deleted file mode 100644 index f088977f056..00000000000 --- a/spec/unit/rendezvous/ecdhv1.spec.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* -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/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts deleted file mode 100644 index 573ae6f2297..00000000000 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* -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 bb3a1943dba..be60ee5c9aa 100644 --- a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -29,7 +29,6 @@ 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", diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index 42e0ee15e9e..f157bbeaef1 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,5 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from "./MSC3903ECDHv1RendezvousChannel"; export * from "./MSC3903ECDHv2RendezvousChannel";