matrix_sdk/authentication/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, Deserializer, Serialize, Serializer};
22use url::Url;
23use vodozemac::Curve25519PublicKey;
24
25#[cfg(doc)]
26use crate::authentication::qrcode::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        homeserver: Url,
41    },
42
43    /// Message declaring which protocols from the previous `m.login.protocols`
44    /// message the new device has picked. Sent by the new device.
45    #[serde(rename = "m.login.protocol")]
46    LoginProtocol {
47        /// The device authorization grant the OIDC provider has given to the
48        /// new device, contains the URL the existing device should use
49        /// to confirm the log in.
50        device_authorization_grant: AuthorizationGrant,
51        /// The protocol the new device has picked.
52        protocol: LoginProtocolType,
53        #[serde(
54            deserialize_with = "deserialize_curve_key",
55            serialize_with = "serialize_curve_key"
56        )]
57        /// The device ID the new device will be using.
58        device_id: Curve25519PublicKey,
59    },
60
61    /// Message declaring that the protocol in the previous `m.login.protocol`
62    /// message was accepted. Sent by the existing device.
63    #[serde(rename = "m.login.protocol_accepted")]
64    LoginProtocolAccepted,
65
66    /// Message that informs the existing device that it successfully obtained
67    /// an access token from the OIDC provider. Sent by the new device.
68    #[serde(rename = "m.login.success")]
69    LoginSuccess,
70
71    /// Message that informs the existing device that the OIDC provider has
72    /// declined to give us an access token, i.e. because the user declined
73    /// the log in. Sent by the new device.
74    #[serde(rename = "m.login.declined")]
75    LoginDeclined,
76
77    /// Message signaling that a failure happened during the login. Can be sent
78    /// by either device.
79    #[serde(rename = "m.login.failure")]
80    LoginFailure {
81        /// The claimed reason for the login failure.
82        reason: LoginFailureReason,
83        /// The homeserver that we attempted to log in to.
84        homeserver: Option<Url>,
85    },
86
87    /// Message containing end-to-end encryption related secrets, the new device
88    /// can use these secrets to mark itself as verified, connect to a room
89    /// key backup, and login other devices via a QR login. Sent by the
90    /// existing device.
91    #[serde(rename = "m.login.secrets")]
92    LoginSecrets(SecretsBundle),
93}
94
95impl QrAuthMessage {
96    /// Create a new [`QrAuthMessage::LoginProtocol`] message with the
97    /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type.
98    pub fn authorization_grant_login_protocol(
99        device_authorization_grant: AuthorizationGrant,
100        device_id: Curve25519PublicKey,
101    ) -> QrAuthMessage {
102        QrAuthMessage::LoginProtocol {
103            device_id,
104            device_authorization_grant,
105            protocol: LoginProtocolType::DeviceAuthorizationGrant,
106        }
107    }
108}
109
110impl From<&StandardDeviceAuthorizationResponse> for AuthorizationGrant {
111    fn from(value: &StandardDeviceAuthorizationResponse) -> Self {
112        Self {
113            verification_uri: value.verification_uri().clone(),
114            verification_uri_complete: value.verification_uri_complete().cloned(),
115        }
116    }
117}
118
119/// Data for the device authorization grant login protocol.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AuthorizationGrant {
122    /// The verification URL the user should open to log the new device in.
123    pub verification_uri: EndUserVerificationUrl,
124
125    /// The verification URL, with the user code pre-filled, which the user
126    /// should open to log the new device in. If this URL is available, the
127    /// user should be presented with it instead of the one in the
128    /// [`AuthorizationGrant::verification_uri`] field.
129    pub verification_uri_complete: Option<VerificationUriComplete>,
130}
131
132/// Reasons why the login might have failed.
133#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
134#[ruma_enum(rename_all = "snake_case")]
135pub enum LoginFailureReason {
136    /// The Device Authorization Grant expired.
137    AuthorizationExpired,
138    /// The device ID specified by the new device already exists in the
139    /// homeserver provided device list.
140    DeviceAlreadyExists,
141    /// The new device is not present in the device list as returned by the
142    /// homeserver.
143    DeviceNotFound,
144    /// Sent by either device to indicate that they received a message of a type
145    /// that they weren't expecting.
146    UnexpectedMessageReceived,
147    /// Sent by a device where no suitable protocol is available or the
148    /// requested protocol requested is not supported.
149    UnsupportedProtocol,
150    /// Sent by either new or existing device to indicate that the user has
151    /// cancelled the login.
152    UserCancelled,
153    #[doc(hidden)]
154    _Custom(PrivOwnedStr),
155}
156
157/// Enum containing known login protocol types.
158#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
159#[ruma_enum(rename_all = "snake_case")]
160pub enum LoginProtocolType {
161    /// The `device_authorization_grant` login protocol type.
162    DeviceAuthorizationGrant,
163    #[doc(hidden)]
164    _Custom(PrivOwnedStr),
165}
166
167// Vodozemac serializes Curve25519 keys directly as a byteslice, while Matrix
168// likes to base64 encode all byte slices.
169//
170// This ensures that we serialize/deserialize in a Matrix-compatible way.
171pub(crate) fn deserialize_curve_key<'de, D>(de: D) -> Result<Curve25519PublicKey, D::Error>
172where
173    D: Deserializer<'de>,
174{
175    let key: String = Deserialize::deserialize(de)?;
176
177    Curve25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom)
178}
179
180pub(crate) fn serialize_curve_key<S>(key: &Curve25519PublicKey, s: S) -> Result<S::Ok, S::Error>
181where
182    S: Serializer,
183{
184    s.serialize_str(&key.to_base64())
185}
186
187#[cfg(test)]
188mod test {
189    use assert_matches2::assert_let;
190    use matrix_sdk_base::crypto::types::BackupSecrets;
191    use serde_json::json;
192    use similar_asserts::assert_eq;
193
194    use super::*;
195
196    #[test]
197    fn test_protocols_serialization() {
198        let json = json!({
199            "type": "m.login.protocols",
200            "protocols": ["device_authorization_grant"],
201            "homeserver": "https://matrix-client.matrix.org/"
202
203        });
204
205        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
206        assert_let!(QrAuthMessage::LoginProtocols { protocols, .. } = &message);
207        assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant));
208
209        let serialized = serde_json::to_value(&message).unwrap();
210        assert_eq!(json, serialized);
211    }
212
213    #[test]
214    fn test_protocol_serialization() {
215        let json = json!({
216            "type": "m.login.protocol",
217            "protocol": "device_authorization_grant",
218            "device_authorization_grant": {
219                "verification_uri_complete": "https://id.matrix.org/device/abcde",
220                "verification_uri": "https://id.matrix.org/device/abcde?code=ABCDE"
221            },
222            "device_id": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
223        });
224        let curve_key =
225            Curve25519PublicKey::from_base64("wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4")
226                .unwrap();
227
228        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
229        assert_let!(QrAuthMessage::LoginProtocol { protocol, device_id, .. } = &message);
230        assert_eq!(protocol, &LoginProtocolType::DeviceAuthorizationGrant);
231        assert_eq!(device_id, &curve_key);
232        let serialized = serde_json::to_value(&message).unwrap();
233        assert_eq!(json, serialized);
234    }
235
236    #[test]
237    fn test_protocol_accepted_serialization() {
238        let json = json!({
239            "type": "m.login.protocol_accepted",
240        });
241
242        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
243        assert_let!(QrAuthMessage::LoginProtocolAccepted = &message);
244        let serialized = serde_json::to_value(&message).unwrap();
245        assert_eq!(json, serialized);
246    }
247
248    #[test]
249    fn test_login_success() {
250        let json = json!({
251            "type": "m.login.success",
252        });
253
254        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
255        assert_let!(QrAuthMessage::LoginSuccess = &message);
256        let serialized = serde_json::to_value(&message).unwrap();
257        assert_eq!(json, serialized);
258    }
259
260    #[test]
261    fn test_login_declined() {
262        let json = json!({
263            "type": "m.login.declined",
264        });
265
266        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
267        assert_let!(QrAuthMessage::LoginDeclined = &message);
268        let serialized = serde_json::to_value(&message).unwrap();
269        assert_eq!(json, serialized);
270    }
271
272    #[test]
273    fn test_login_failure() {
274        let json = json!({
275            "type": "m.login.failure",
276            "reason": "unsupported_protocol",
277            "homeserver": "https://matrix-client.matrix.org/"
278        });
279
280        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
281        assert_let!(QrAuthMessage::LoginFailure { reason, .. } = &message);
282        assert_eq!(reason, &LoginFailureReason::UnsupportedProtocol);
283        let serialized = serde_json::to_value(&message).unwrap();
284        assert_eq!(json, serialized);
285    }
286
287    #[test]
288    fn test_login_secrets() {
289        let json = json!({
290            "type": "m.login.secrets",
291            "cross_signing": {
292                "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw",
293                "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk",
294                "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM",
295            },
296            "backup": {
297                "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
298                "backup_version": "2",
299                "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
300            },
301        });
302
303        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
304        assert_let!(
305            QrAuthMessage::LoginSecrets(SecretsBundle { cross_signing, backup }) = &message
306        );
307        assert_eq!(cross_signing.master_key, "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw");
308        assert_eq!(cross_signing.self_signing_key, "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk");
309        assert_eq!(cross_signing.user_signing_key, "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM");
310
311        assert_let!(Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = backup);
312        assert_eq!(backup.backup_version, "2");
313        assert_eq!(&backup.key.to_base64(), "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
314
315        let serialized = serde_json::to_value(&message).unwrap();
316        assert_eq!(json, serialized);
317    }
318}