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