Skip to main content

matrix_sdk/authentication/oauth/qrcode/
messages.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use matrix_sdk_base::crypto::types::SecretsBundle;
16use matrix_sdk_common::deserialized_responses::PrivOwnedStr;
17use oauth2::{
18    EndUserVerificationUrl, StandardDeviceAuthorizationResponse, VerificationUriComplete,
19};
20use ruma::serde::StringEnum;
21use serde::{Deserialize, Serialize};
22use url::Url;
23use vodozemac::Curve25519PublicKey;
24
25#[cfg(doc)]
26use super::QRCodeLoginError::SecureChannel;
27
28/// Messages that will be exchanged over the [`SecureChannel`] to log in a new
29/// device using a QR code.
30#[derive(Debug, Serialize, Deserialize)]
31#[serde(tag = "type")]
32pub enum QrAuthMessage {
33    /// Message declaring the available protocols for sign in. Sent by the
34    /// existing device.
35    #[serde(rename = "m.login.protocols")]
36    LoginProtocols {
37        /// The login protocols the existing device supports.
38        protocols: Vec<LoginProtocolType>,
39        /// The homeserver we're going to log in to.
40        ///
41        /// Note: this doesn't match the MSC which says that it is a server name
42        /// not a full URL
43        homeserver: Url,
44    },
45
46    /// Message declaring which protocols from the previous `m.login.protocols`
47    /// message the new device has picked. Sent by the new device.
48    #[serde(rename = "m.login.protocol")]
49    LoginProtocol {
50        /// The device authorization grant the OAuth 2.0 server has given to the
51        /// new device, contains the URL the existing device should use to
52        /// confirm the log in.
53        device_authorization_grant: AuthorizationGrant,
54        /// The protocol the new device has picked.
55        protocol: LoginProtocolType,
56        /// The device ID the new device will be using.
57        device_id: String,
58    },
59
60    /// Message declaring that the protocol in the previous `m.login.protocol`
61    /// message was accepted. Sent by the existing device.
62    #[serde(rename = "m.login.protocol_accepted")]
63    LoginProtocolAccepted,
64
65    /// Message that informs the existing device that it successfully obtained
66    /// an access token from the OAuth 2.0 server. Sent by the new device.
67    #[serde(rename = "m.login.success")]
68    LoginSuccess,
69
70    /// Message that informs the existing device that the OAuth 2.0 server has
71    /// declined to give us an access token, i.e. because the user declined the
72    /// log in. Sent by the new device.
73    #[serde(rename = "m.login.declined")]
74    LoginDeclined,
75
76    /// Message signaling that a failure happened during the login. Can be sent
77    /// by either device.
78    #[serde(rename = "m.login.failure")]
79    LoginFailure {
80        /// The claimed reason for the login failure.
81        reason: LoginFailureReason,
82        /// The homeserver that we attempted to log in to.
83        homeserver: Option<Url>,
84    },
85
86    /// Message containing end-to-end encryption related secrets, the new device
87    /// can use these secrets to mark itself as verified, connect to a room key
88    /// backup, and login other devices via a QR login. Sent by the existing
89    /// device.
90    #[serde(rename = "m.login.secrets")]
91    LoginSecrets(SecretsBundle),
92}
93
94impl QrAuthMessage {
95    /// Create a new [`QrAuthMessage::LoginProtocol`] message with the
96    /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type.
97    pub fn authorization_grant_login_protocol(
98        device_authorization_grant: AuthorizationGrant,
99        device_id: Curve25519PublicKey,
100    ) -> QrAuthMessage {
101        QrAuthMessage::LoginProtocol {
102            device_id: device_id.to_base64(),
103            device_authorization_grant,
104            protocol: LoginProtocolType::DeviceAuthorizationGrant,
105        }
106    }
107}
108
109impl From<&StandardDeviceAuthorizationResponse> for AuthorizationGrant {
110    fn from(value: &StandardDeviceAuthorizationResponse) -> Self {
111        Self {
112            verification_uri: value.verification_uri().clone(),
113            verification_uri_complete: value.verification_uri_complete().cloned(),
114        }
115    }
116}
117
118/// Data for the device authorization grant login protocol.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct AuthorizationGrant {
121    /// The verification URL the user should open to log the new device in.
122    pub verification_uri: EndUserVerificationUrl,
123
124    /// The verification URL, with the user code pre-filled, which the user
125    /// should open to log the new device in. If this URL is available, the user
126    /// should be presented with it instead of the one in the
127    /// [`AuthorizationGrant::verification_uri`] field.
128    pub verification_uri_complete: Option<VerificationUriComplete>,
129}
130
131/// Reasons why the login might have failed.
132#[derive(Clone, StringEnum)]
133#[ruma_enum(rename_all = "snake_case")]
134pub enum LoginFailureReason {
135    /// The Device Authorization Grant expired.
136    AuthorizationExpired,
137    /// The device ID specified by the new device already exists in the
138    /// homeserver provided device list.
139    DeviceAlreadyExists,
140    /// The new device is not present in the device list as returned by the
141    /// homeserver.
142    DeviceNotFound,
143    /// Sent by either device to indicate that they received a message of a type
144    /// that they weren't expecting.
145    UnexpectedMessageReceived,
146    /// Sent by a device where no suitable protocol is available or the
147    /// requested protocol requested is not supported.
148    UnsupportedProtocol,
149    /// Sent by either new or existing device to indicate that the user has
150    /// cancelled the login.
151    UserCancelled,
152    #[doc(hidden)]
153    _Custom(PrivOwnedStr),
154}
155
156/// Enum containing known login protocol types.
157#[derive(Clone, StringEnum)]
158#[ruma_enum(rename_all = "snake_case")]
159pub enum LoginProtocolType {
160    /// The `device_authorization_grant` login protocol type.
161    DeviceAuthorizationGrant,
162    #[doc(hidden)]
163    _Custom(PrivOwnedStr),
164}
165
166#[cfg(test)]
167mod test {
168    use matrix_sdk_base::crypto::types::BackupSecrets;
169    use serde_json::json;
170    use similar_asserts::assert_eq;
171    use strass::assert_let;
172
173    use super::*;
174
175    #[test]
176    fn test_protocols_serialization() {
177        let json = json!({
178            "type": "m.login.protocols",
179            "protocols": ["device_authorization_grant"],
180            "homeserver": "https://matrix-client.matrix.org/"
181
182        });
183
184        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
185        assert_let!(QrAuthMessage::LoginProtocols { protocols, .. } = &message);
186        assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant));
187
188        let serialized = serde_json::to_value(&message).unwrap();
189        assert_eq!(json, serialized);
190    }
191
192    #[test]
193    fn test_protocol_serialization() {
194        let json = json!({
195            "type": "m.login.protocol",
196            "protocol": "device_authorization_grant",
197            "device_authorization_grant": {
198                "verification_uri_complete": "https://id.matrix.org/device/abcde",
199                "verification_uri": "https://id.matrix.org/device/abcde?code=ABCDE"
200            },
201            "device_id": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
202        });
203
204        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
205        assert_let!(QrAuthMessage::LoginProtocol { protocol, device_id, .. } = &message);
206        assert_eq!(protocol, &LoginProtocolType::DeviceAuthorizationGrant);
207        assert_eq!(device_id, "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4");
208        let serialized = serde_json::to_value(&message).unwrap();
209        assert_eq!(json, serialized);
210    }
211
212    #[test]
213    fn test_protocol_accepted_serialization() {
214        let json = json!({
215            "type": "m.login.protocol_accepted",
216        });
217
218        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
219        assert_let!(QrAuthMessage::LoginProtocolAccepted = &message);
220        let serialized = serde_json::to_value(&message).unwrap();
221        assert_eq!(json, serialized);
222    }
223
224    #[test]
225    fn test_login_success() {
226        let json = json!({
227            "type": "m.login.success",
228        });
229
230        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
231        assert_let!(QrAuthMessage::LoginSuccess = &message);
232        let serialized = serde_json::to_value(&message).unwrap();
233        assert_eq!(json, serialized);
234    }
235
236    #[test]
237    fn test_login_declined() {
238        let json = json!({
239            "type": "m.login.declined",
240        });
241
242        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
243        assert_let!(QrAuthMessage::LoginDeclined = &message);
244        let serialized = serde_json::to_value(&message).unwrap();
245        assert_eq!(json, serialized);
246    }
247
248    #[test]
249    fn test_login_failure() {
250        let json = json!({
251            "type": "m.login.failure",
252            "reason": "unsupported_protocol",
253            "homeserver": "https://matrix-client.matrix.org/"
254        });
255
256        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
257        assert_let!(QrAuthMessage::LoginFailure { reason, .. } = &message);
258        assert_eq!(reason, &LoginFailureReason::UnsupportedProtocol);
259        let serialized = serde_json::to_value(&message).unwrap();
260        assert_eq!(json, serialized);
261    }
262
263    #[test]
264    fn test_login_secrets() {
265        let json = json!({
266            "type": "m.login.secrets",
267            "cross_signing": {
268                "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw",
269                "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk",
270                "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM",
271            },
272            "backup": {
273                "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
274                "backup_version": "2",
275                "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
276            },
277        });
278
279        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
280        assert_let!(
281            QrAuthMessage::LoginSecrets(SecretsBundle { cross_signing, backup }) = &message
282        );
283        assert_eq!(cross_signing.master_key, "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw");
284        assert_eq!(cross_signing.self_signing_key, "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk");
285        assert_eq!(cross_signing.user_signing_key, "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM");
286
287        assert_let!(Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = backup);
288        assert_eq!(backup.backup_version, "2");
289        assert_eq!(&backup.key.to_base64(), "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
290
291        let serialized = serde_json::to_value(&message).unwrap();
292        assert_eq!(json, serialized);
293    }
294}