@@ -31,18 +31,48 @@ import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
31
31
import { logger } from "../logger" ;
32
32
import { sleep } from "../utils" ;
33
33
34
+ /**
35
+ * These are the possible types of payload that are used in
36
+ * [MSC3906](https://github.com/matrix-org/matrix-spec-proposals/pull/3906) payloads.
37
+ * The values are used in the `type` field.
38
+ */
34
39
enum PayloadType {
35
40
/**
36
41
* @deprecated Only used in MSC3906 v1
37
42
*/
38
43
Finish = "m.login.finish" ,
44
+ /**
45
+ * Indicates that a new device is ready to proceed with the setup process.
46
+ */
39
47
Progress = "m.login.progress" ,
48
+ /**
49
+ * Used by the new device to indicate which protocol to use.
50
+ */
40
51
Protocol = "m.login.protocol" ,
52
+ /**
53
+ * Used for the new device to indicate which protocols are supported by the existing device and
54
+ * homeserver.
55
+ */
41
56
Protocols = "m.login.protocols" ,
57
+ /**
58
+ * Indicates that the sign of the new device was approved by the user on the existing device.
59
+ */
42
60
Approved = "m.login.approved" ,
61
+ /**
62
+ * Indicates that the new device has signed in successfully.
63
+ */
43
64
Success = "m.login.success" ,
65
+ /**
66
+ * Indicates that the new device has been successfully verified by the existing device.
67
+ */
44
68
Verified = "m.login.verified" ,
69
+ /**
70
+ * Indicates that the login failed.
71
+ */
45
72
Failure = "m.login.failure" ,
73
+ /**
74
+ * Indicates that the user declined the login on the existing device.
75
+ */
46
76
Declined = "m.login.declined" ,
47
77
}
48
78
@@ -57,14 +87,21 @@ enum Outcome {
57
87
Unsupported = "unsupported" ,
58
88
}
59
89
90
+ /**
91
+ * Used in the `reason` field of the `m.login.failure` payload.
92
+ */
60
93
enum FailureReason {
61
94
Cancelled = "cancelled" ,
62
95
Unsupported = "unsupported" ,
63
96
E2EESecurityError = "e2ee_security_error" ,
64
97
IncompatibleIntent = "incompatible_intent" ,
65
98
}
66
99
100
+ /**
101
+ * This represents an [MSC3906](https://github.com/matrix-org/matrix-spec-proposals/pull/3906) payload.
102
+ */
67
103
export interface MSC3906RendezvousPayload {
104
+ /** The type of the payload */
68
105
type : PayloadType ;
69
106
intent ?: RendezvousIntent ;
70
107
/**
@@ -83,57 +120,72 @@ export interface MSC3906RendezvousPayload {
83
120
homeserver ?: string ;
84
121
}
85
122
123
+ /**
124
+ * Represents the use of an `m.login.token` obtained from an existing device to sign in on a new device.
125
+ */
86
126
const LOGIN_TOKEN_PROTOCOL = new UnstableValue ( "login_token" , "org.matrix.msc3906.login_token" ) ;
87
127
88
128
/**
89
- * Implements MSC3906 to allow a user to sign in on a new device using QR code.
90
- * This implementation only supports generating a QR code on a device that is already signed in.
129
+ * This class can be used to complete a "rendezvous flow" as defined in MSC3906.
130
+ *
131
+ * Currently it only supports being used on a device that is already signed in that wishes to help sign in
132
+ * another device.
133
+ *
91
134
* Note that this is UNSTABLE and may have breaking changes without notice.
92
135
*/
93
136
export class MSC3906Rendezvous {
94
137
private newDeviceId ?: string ;
95
138
private newDeviceKey ?: string ;
96
139
private ourIntent : RendezvousIntent = RendezvousIntent . RECIPROCATE_LOGIN_ON_EXISTING_DEVICE ;
97
- private v1FallbackEnabled : boolean ;
140
+ // if true then we follow the v1 flow, otherwise we follow the v2 flow
141
+ private usingV1Flow : boolean ;
98
142
private _code ?: string ;
99
143
100
144
/**
101
- * @param channel - The secure channel used for communication
102
- * @param client - The Matrix client in used on the device already logged in
103
- * @param onFailure - Callback for when the rendezvous fails
104
- * @param flow - The flow to use. Defaults to MSC3906 v1 for backwards compatibility.
145
+ * Creates an instance that can be used to manage the execution of a rendezvous flow.
146
+ *
147
+ * @param channel - The rendezvous channel that should be used for communication with the other device
148
+ * @param client - The Matrix client that should be used.
149
+ * @param onFailure - Optional callback function to be notified of rendezvous failures.
150
+ * @param flow - The rendezvous flow to use. Defaults to setting up an additional device using MSC3906 v1,
151
+ * for backwards compatibility.
105
152
*/
106
153
public constructor (
107
154
private channel : RendezvousChannel < MSC3906RendezvousPayload > ,
108
155
private client : MatrixClient ,
109
156
public onFailure ?: RendezvousFailureListener ,
110
157
private flow : RendezvousFlow = SETUP_ADDITIONAL_DEVICE_FLOW_V1 ,
111
158
) {
112
- this . v1FallbackEnabled = flow === SETUP_ADDITIONAL_DEVICE_FLOW_V1 ;
159
+ this . usingV1Flow = flow === SETUP_ADDITIONAL_DEVICE_FLOW_V1 ;
113
160
}
114
161
115
162
/**
116
- * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
163
+ * @returns The code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
117
164
*/
118
165
public get code ( ) : string | undefined {
119
166
return this . _code ;
120
167
}
121
168
122
169
/**
123
- * Generate the code including doing partial set up of the channel where required.
170
+ * Generate the code including doing partial set up of the channel where required. This code could be encoded in a QR.
124
171
*/
125
172
public async generateCode ( ) : Promise < void > {
126
173
if ( this . _code ) {
127
174
return ;
128
175
}
129
176
130
- const raw = this . v1FallbackEnabled
177
+ const raw = this . usingV1Flow
131
178
? await this . channel . generateCode ( this . ourIntent )
132
179
: await this . channel . generateCode ( this . ourIntent , this . flow ) ;
133
180
this . _code = JSON . stringify ( raw ) ;
134
181
}
135
182
136
183
/**
184
+ * Call this after the code has been shown to the user (perhaps in a QR). It will poll for the other device
185
+ * at the rendezvous point and start the process of setting up the new device.
186
+ *
187
+ * If successful then the user should be asked to approve the login of the other device whilst displaying the
188
+ * returned checksum code which the user should verify matches the code shown on the other device.
137
189
*
138
190
* @returns the checksum of the secure channel if the rendezvous set up was successful, otherwise undefined
139
191
*/
@@ -147,7 +199,7 @@ export class MSC3906Rendezvous {
147
199
if ( features . get ( Feature . LoginTokenRequest ) === ServerSupport . Unsupported ) {
148
200
logger . info ( "Server doesn't support MSC3882" ) ;
149
201
await this . send (
150
- this . v1FallbackEnabled
202
+ this . usingV1Flow
151
203
? { type : PayloadType . Finish , outcome : Outcome . Unsupported }
152
204
: { type : PayloadType . Failure , reason : FailureReason . Unsupported } ,
153
205
) ;
@@ -156,16 +208,21 @@ export class MSC3906Rendezvous {
156
208
}
157
209
158
210
await this . send ( {
159
- type : this . v1FallbackEnabled ? PayloadType . Progress : PayloadType . Protocols ,
211
+ type : this . usingV1Flow ? PayloadType . Progress : PayloadType . Protocols ,
160
212
protocols : [ LOGIN_TOKEN_PROTOCOL . name ] ,
161
213
} ) ;
162
214
163
215
logger . info ( "Waiting for other device to chose protocol" ) ;
164
216
const nextPayload = await this . receive ( ) ;
165
217
166
- this . checkForV1Fallback ( nextPayload ) ;
218
+ // even if we didn't start in v1 mode we might detect that the other device is v1:
219
+ // - the finish payload is only used in v1
220
+ // - a progress payload is only sent at this point in v1, in v2 the use of it is different
221
+ if ( nextPayload . type === PayloadType . Finish || nextPayload . type === PayloadType . Progress ) {
222
+ this . usingV1Flow = true ;
223
+ }
167
224
168
- const protocol = this . v1FallbackEnabled
225
+ const protocol = this . usingV1Flow
169
226
? await this . handleV1ProtocolPayload ( nextPayload )
170
227
: await this . handleV2ProtocolPayload ( nextPayload ) ;
171
228
@@ -178,18 +235,6 @@ export class MSC3906Rendezvous {
178
235
return checksum ;
179
236
}
180
237
181
- private checkForV1Fallback ( { type } : MSC3906RendezvousPayload ) : void {
182
- // even if we didn't start in v1 fallback we might detect that the other device is v1
183
- if ( type === PayloadType . Finish || type === PayloadType . Progress ) {
184
- // this is a PDU from a v1 flow so use fallback mode
185
- this . v1FallbackEnabled = true ;
186
- }
187
- }
188
-
189
- /**
190
- *
191
- * @returns true if the protocol was received successfully, false otherwise
192
- */
193
238
private async handleV1ProtocolPayload ( {
194
239
type,
195
240
protocol,
@@ -223,10 +268,6 @@ export class MSC3906Rendezvous {
223
268
return protocol ;
224
269
}
225
270
226
- /**
227
- *
228
- * @returns true if the protocol was received successfully, false otherwise
229
- */
230
271
private async handleV2ProtocolPayload ( {
231
272
type,
232
273
protocol,
@@ -275,18 +316,26 @@ export class MSC3906Rendezvous {
275
316
await this . channel . send ( payload ) ;
276
317
}
277
318
319
+ /**
320
+ * Call this if the user has declined the login.
321
+ */
278
322
public async declineLoginOnExistingDevice ( ) : Promise < void > {
279
323
logger . info ( "User declined sign in" ) ;
280
324
await this . send (
281
- this . v1FallbackEnabled
282
- ? { type : PayloadType . Finish , outcome : Outcome . Declined }
283
- : { type : PayloadType . Declined } ,
325
+ this . usingV1Flow ? { type : PayloadType . Finish , outcome : Outcome . Declined } : { type : PayloadType . Declined } ,
284
326
) ;
285
327
}
286
328
329
+ /**
330
+ * Call this if the user has approved the login.
331
+ *
332
+ * @param loginToken - the login token to send to the new device for it to complete the login flow
333
+ * @returns if the new device successfully completed the login flow and provided their device id then the device id is
334
+ * returned, otherwise undefined
335
+ */
287
336
public async approveLoginOnExistingDevice ( loginToken : string ) : Promise < string | undefined > {
288
337
await this . channel . send ( {
289
- type : this . v1FallbackEnabled ? PayloadType . Progress : PayloadType . Approved ,
338
+ type : this . usingV1Flow ? PayloadType . Progress : PayloadType . Approved ,
290
339
login_token : loginToken ,
291
340
homeserver : this . client . baseUrl ,
292
341
} ) ;
@@ -298,10 +347,7 @@ export class MSC3906Rendezvous {
298
347
}
299
348
const { type, outcome, device_id : deviceId , device_key : deviceKey } = res ;
300
349
301
- if (
302
- ( this . v1FallbackEnabled && outcome !== "success" ) ||
303
- ( ! this . v1FallbackEnabled && type !== PayloadType . Success )
304
- ) {
350
+ if ( ( this . usingV1Flow && outcome !== "success" ) || ( ! this . usingV1Flow && type !== PayloadType . Success ) ) {
305
351
throw new Error ( "Linking failed" ) ;
306
352
}
307
353
@@ -339,8 +385,8 @@ export class MSC3906Rendezvous {
339
385
const masterPublicKey = this . client . crypto . crossSigningInfo . getId ( "master" ) ! ;
340
386
341
387
await this . send ( {
342
- type : this . v1FallbackEnabled ? PayloadType . Finish : PayloadType . Verified ,
343
- outcome : this . v1FallbackEnabled ? Outcome . Verified : undefined ,
388
+ type : this . usingV1Flow ? PayloadType . Finish : PayloadType . Verified ,
389
+ outcome : this . usingV1Flow ? Outcome . Verified : undefined ,
344
390
verifying_device_id : this . client . getDeviceId ( ) ! ,
345
391
verifying_device_key : this . client . getDeviceEd25519Key ( ) ! ,
346
392
master_key : masterPublicKey ,
@@ -350,7 +396,8 @@ export class MSC3906Rendezvous {
350
396
}
351
397
352
398
/**
353
- * Verify the device and cross-sign it.
399
+ * Wait for a device to be visible via the homeserver and then verify/cross-sign it.
400
+ *
354
401
* @param timeout - time in milliseconds to wait for device to come online
355
402
* @returns the new device info if the device was verified
356
403
*/
0 commit comments