-
-
Notifications
You must be signed in to change notification settings - Fork 619
Support for MSC3906 v2 #3193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for MSC3906 v2 #3193
Changes from 10 commits
3639083
b1d7788
d4e1c9f
935632b
59ee0a2
f9324d3
ccb6e1f
7bb10a1
3120ab5
b6699ba
066006d
c4ea55b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,14 @@ limitations under the License. | |
|
||
import { UnstableValue } from "matrix-events-sdk"; | ||
|
||
import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from "."; | ||
import { | ||
RendezvousChannel, | ||
RendezvousFailureListener, | ||
RendezvousFailureReason, | ||
RendezvousFlow, | ||
RendezvousIntent, | ||
SETUP_ADDITIONAL_DEVICE_FLOW_V1, | ||
} from "."; | ||
import { MatrixClient } from "../client"; | ||
import { CrossSigningInfo } from "../crypto/CrossSigning"; | ||
import { DeviceInfo } from "../crypto/deviceinfo"; | ||
|
@@ -25,11 +32,23 @@ import { logger } from "../logger"; | |
import { sleep } from "../utils"; | ||
|
||
enum PayloadType { | ||
Start = "m.login.start", | ||
/** | ||
* @deprecated Only used in MSC3906 v1 | ||
*/ | ||
Finish = "m.login.finish", | ||
Progress = "m.login.progress", | ||
Protocol = "m.login.protocol", | ||
Protocols = "m.login.protocols", | ||
Approved = "m.login.approved", | ||
Success = "m.login.success", | ||
Verified = "m.login.verified", | ||
Failure = "m.login.failure", | ||
Declined = "m.login.declined", | ||
} | ||
|
||
/** | ||
* @deprecated Only used in MSC3906 v1 | ||
*/ | ||
enum Outcome { | ||
Success = "success", | ||
Failure = "failure", | ||
|
@@ -38,10 +57,21 @@ enum Outcome { | |
Unsupported = "unsupported", | ||
} | ||
|
||
enum FailureReason { | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Cancelled = "cancelled", | ||
Unsupported = "unsupported", | ||
E2EESecurityError = "e2ee_security_error", | ||
IncompatibleIntent = "incompatible_intent", | ||
} | ||
|
||
export interface MSC3906RendezvousPayload { | ||
type: PayloadType; | ||
intent?: RendezvousIntent; | ||
/** | ||
* @deprecated Only used in MSC3906 v1. Instead the type field should be used in future | ||
*/ | ||
outcome?: Outcome; | ||
reason?: FailureReason; | ||
device_id?: string; | ||
device_key?: string; | ||
verifying_device_id?: string; | ||
|
@@ -64,18 +94,23 @@ export class MSC3906Rendezvous { | |
private newDeviceId?: string; | ||
private newDeviceKey?: string; | ||
private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; | ||
private v1FallbackEnabled: boolean; | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private _code?: string; | ||
|
||
/** | ||
* @param channel - The secure channel used for communication | ||
* @param client - The Matrix client in used on the device already logged in | ||
* @param onFailure - Callback for when the rendezvous fails | ||
* @param flow - The flow to use. Defaults to MSC3906 v1 for backwards compatibility. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please could we have some more words explaining what a "flow" means and how I, as a user of this API, should pick one? |
||
*/ | ||
public constructor( | ||
private channel: RendezvousChannel<MSC3906RendezvousPayload>, | ||
private client: MatrixClient, | ||
public onFailure?: RendezvousFailureListener, | ||
) {} | ||
private flow: RendezvousFlow = SETUP_ADDITIONAL_DEVICE_FLOW_V1, | ||
) { | ||
this.v1FallbackEnabled = flow === SETUP_ADDITIONAL_DEVICE_FLOW_V1; | ||
} | ||
|
||
/** | ||
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. | ||
|
@@ -92,9 +127,16 @@ export class MSC3906Rendezvous { | |
return; | ||
} | ||
|
||
this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); | ||
const raw = this.v1FallbackEnabled | ||
? await this.channel.generateCode(this.ourIntent) | ||
: await this.channel.generateCode(this.ourIntent, this.flow); | ||
this._code = JSON.stringify(raw); | ||
} | ||
|
||
/** | ||
* | ||
* @returns the checksum of the secure channel if the rendezvous set up was successful, otherwise undefined | ||
*/ | ||
public async startAfterShowingCode(): Promise<string | undefined> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Particularly given this is a public function, please could it have a better doc-comment? What does it actually do, beyond returning a success flag? |
||
const checksum = await this.channel.connect(); | ||
|
||
|
@@ -104,39 +146,125 @@ export class MSC3906Rendezvous { | |
// determine available protocols | ||
if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { | ||
logger.info("Server doesn't support MSC3882"); | ||
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported }); | ||
await this.send( | ||
this.v1FallbackEnabled | ||
? { type: PayloadType.Finish, outcome: Outcome.Unsupported } | ||
: { type: PayloadType.Failure, reason: FailureReason.Unsupported }, | ||
); | ||
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); | ||
return undefined; | ||
} | ||
|
||
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] }); | ||
await this.send({ | ||
type: this.v1FallbackEnabled ? PayloadType.Progress : PayloadType.Protocols, | ||
protocols: [LOGIN_TOKEN_PROTOCOL.name], | ||
}); | ||
|
||
logger.info("Waiting for other device to chose protocol"); | ||
const { type, protocol, outcome } = await this.receive(); | ||
const nextPayload = await this.receive(); | ||
|
||
this.checkForV1Fallback(nextPayload); | ||
|
||
const protocol = this.v1FallbackEnabled | ||
? await this.handleV1ProtocolPayload(nextPayload) | ||
: await this.handleV2ProtocolPayload(nextPayload); | ||
|
||
// invalid protocol | ||
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) { | ||
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); | ||
return undefined; | ||
} | ||
|
||
return checksum; | ||
} | ||
|
||
private checkForV1Fallback({ type }: MSC3906RendezvousPayload): void { | ||
// even if we didn't start in v1 fallback we might detect that the other device is v1 | ||
if (type === PayloadType.Finish || type === PayloadType.Progress) { | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// this is a PDU from a v1 flow so use fallback mode | ||
this.v1FallbackEnabled = true; | ||
} | ||
} | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* | ||
* @returns true if the protocol was received successfully, false otherwise | ||
*/ | ||
private async handleV1ProtocolPayload({ | ||
type, | ||
protocol, | ||
outcome, | ||
reason, | ||
intent, | ||
}: MSC3906RendezvousPayload): Promise<string | void> { | ||
if (type === PayloadType.Finish) { | ||
// new device decided not to complete | ||
switch (outcome ?? "") { | ||
case "unsupported": | ||
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); | ||
break; | ||
default: | ||
await this.cancel(RendezvousFailureReason.Unknown); | ||
let reason: RendezvousFailureReason; | ||
if (intent) { | ||
reason = | ||
this.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE | ||
? RendezvousFailureReason.OtherDeviceNotSignedIn | ||
: RendezvousFailureReason.OtherDeviceAlreadySignedIn; | ||
} else if (outcome === Outcome.Unsupported) { | ||
reason = RendezvousFailureReason.UnsupportedAlgorithm; | ||
} else { | ||
reason = RendezvousFailureReason.Unknown; | ||
} | ||
return undefined; | ||
await this.cancel(reason); | ||
return; | ||
} | ||
|
||
// unexpected payload | ||
if (type !== PayloadType.Progress) { | ||
await this.cancel(RendezvousFailureReason.Unknown); | ||
return undefined; | ||
return; | ||
} | ||
|
||
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) { | ||
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); | ||
return undefined; | ||
return protocol; | ||
} | ||
|
||
/** | ||
* | ||
* @returns true if the protocol was received successfully, false otherwise | ||
*/ | ||
private async handleV2ProtocolPayload({ | ||
type, | ||
protocol, | ||
outcome, | ||
reason, | ||
intent, | ||
}: MSC3906RendezvousPayload): Promise<string | void> { | ||
// v2 flow | ||
if (type === PayloadType.Failure) { | ||
// new device decided not to complete | ||
let failureReason: RendezvousFailureReason; | ||
switch (reason ?? "") { | ||
case FailureReason.Cancelled: | ||
failureReason = RendezvousFailureReason.UserCancelled; | ||
break; | ||
case FailureReason.IncompatibleIntent: | ||
failureReason = | ||
this.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE | ||
? RendezvousFailureReason.OtherDeviceNotSignedIn | ||
: RendezvousFailureReason.OtherDeviceAlreadySignedIn; | ||
break; | ||
case FailureReason.Unsupported: | ||
failureReason = RendezvousFailureReason.UnsupportedAlgorithm; | ||
break; | ||
default: | ||
failureReason = RendezvousFailureReason.Unknown; | ||
} | ||
await this.cancel(failureReason); | ||
return; | ||
} | ||
|
||
return checksum; | ||
// unexpected payload | ||
if (type !== PayloadType.Protocol) { | ||
await this.cancel(RendezvousFailureReason.Unknown); | ||
return; | ||
} | ||
|
||
return protocol; | ||
} | ||
|
||
private async receive(): Promise<MSC3906RendezvousPayload> { | ||
|
@@ -149,21 +277,31 @@ export class MSC3906Rendezvous { | |
|
||
public async declineLoginOnExistingDevice(): Promise<void> { | ||
logger.info("User declined sign in"); | ||
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined }); | ||
await this.send( | ||
this.v1FallbackEnabled | ||
? { type: PayloadType.Finish, outcome: Outcome.Declined } | ||
: { type: PayloadType.Declined }, | ||
); | ||
} | ||
|
||
public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> { | ||
// eslint-disable-next-line camelcase | ||
await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); | ||
await this.channel.send({ | ||
type: this.v1FallbackEnabled ? PayloadType.Progress : PayloadType.Approved, | ||
login_token: loginToken, | ||
homeserver: this.client.baseUrl, | ||
}); | ||
|
||
logger.info("Waiting for outcome"); | ||
const res = await this.receive(); | ||
if (!res) { | ||
return undefined; | ||
} | ||
const { outcome, device_id: deviceId, device_key: deviceKey } = res; | ||
const { type, outcome, device_id: deviceId, device_key: deviceKey } = res; | ||
|
||
if (outcome !== "success") { | ||
if ( | ||
(this.v1FallbackEnabled && outcome !== "success") || | ||
(!this.v1FallbackEnabled && type !== PayloadType.Success) | ||
) { | ||
throw new Error("Linking failed"); | ||
} | ||
|
||
|
@@ -201,8 +339,8 @@ export class MSC3906Rendezvous { | |
const masterPublicKey = this.client.crypto.crossSigningInfo.getId("master")!; | ||
|
||
await this.send({ | ||
type: PayloadType.Finish, | ||
outcome: Outcome.Verified, | ||
type: this.v1FallbackEnabled ? PayloadType.Finish : PayloadType.Verified, | ||
outcome: this.v1FallbackEnabled ? Outcome.Verified : undefined, | ||
verifying_device_id: this.client.getDeviceId()!, | ||
verifying_device_key: this.client.getDeviceEd25519Key()!, | ||
master_key: masterPublicKey, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
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 { UnstableValue } from "../NamespacedValue"; | ||
|
||
export const SETUP_ADDITIONAL_DEVICE_FLOW_V1 = "org.matrix.msc3906.v1"; | ||
|
||
export const SETUP_ADDITIONAL_DEVICE_FLOW_V2 = new UnstableValue( | ||
"m.setup.additional_device.v2", | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"org.matrix.msc3906.setup.additional_device.v2", | ||
); | ||
|
||
// v1 is never included in the JSON, but we give it a name for the sake of determining the flow to use | ||
export type RendezvousFlow = | ||
hughns marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| typeof SETUP_ADDITIONAL_DEVICE_FLOW_V2.name | ||
| typeof SETUP_ADDITIONAL_DEVICE_FLOW_V2.altName | ||
| typeof SETUP_ADDITIONAL_DEVICE_FLOW_V1; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ import { | |
RendezvousTransportDetails, | ||
RendezvousTransport, | ||
RendezvousFailureReason, | ||
RendezvousFlow, | ||
} from ".."; | ||
import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib"; | ||
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; | ||
|
@@ -85,7 +86,7 @@ export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> { | |
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); | ||
} | ||
|
||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> { | ||
public async generateCode(intent: RendezvousIntent, flow?: RendezvousFlow): Promise<ECDHv2RendezvousCode> { | ||
if (this.transport.ready) { | ||
throw new Error("Code already generated"); | ||
} | ||
|
@@ -98,6 +99,7 @@ export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> { | |
key: encodeUnpaddedBase64(this.ourPublicKey), | ||
transport: await this.transport.details(), | ||
}, | ||
flow, | ||
intent, | ||
Comment on lines
+102
to
103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}; | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.