atagen

macltok

public
Star0
Watch0
fa9114fcf75f7bdf1a7cec3b3367d604cf7de472
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/account.rsRust326 lines10.3 KB
1
use std::{
2
    collections::HashMap,
3
    path::{Path, PathBuf},
4
};
5
6
use matrix_sdk::{
7
    Client, ClientBuilder, ThreadingSupport,
8
    authentication::{
9
        AuthSession,
10
        matrix::MatrixSession,
11
        oauth::{
12
            ClientId, ClientRegistrationData, CsrfToken, OAuthSession, UserSession,
13
            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
14
        },
15
    },
16
    encryption::{BackupDownloadStrategy, EncryptionSettings},
17
    ruma::{OwnedDeviceId, serde::Raw},
18
};
19
use serde::{Deserialize, Serialize};
20
use thiserror::Error;
21
use url::Url;
22
23
use crate::{ClientHandle, Engine, EngineConfig, EngineError, EventStream};
24
25
pub struct Session {
26
    client: Client,
27
    record: SessionRecord,
28
}
29
30
pub struct PasswordLogin {
31
    pub homeserver: String,
32
    pub username: String,
33
    pub password: String,
34
    pub device_display_name: Option<String>,
35
    /// set after a soft logout so the store's keys still belong to us
36
    pub device_id: Option<OwnedDeviceId>,
37
}
38
39
pub struct OAuthLogin {
40
    pub homeserver: String,
41
    pub redirect_uri: String,
42
    pub client_uri: String,
43
    pub client_name: Option<String>,
44
    pub client_id: Option<String>,
45
    pub device_id: Option<OwnedDeviceId>,
46
    pub application_type: OAuthApplicationType,
47
}
48
49
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50
pub enum OAuthApplicationType {
51
    Native,
52
    Web,
53
}
54
55
pub struct PendingOAuthLogin {
56
    client: Client,
57
    authorization_url: String,
58
    state: Option<CsrfToken>,
59
}
60
61
impl PendingOAuthLogin {
62
    #[must_use]
63
    pub fn authorization_url(&self) -> &str {
64
        &self.authorization_url
65
    }
66
67
    #[must_use]
68
    pub fn registered_client_id(&self) -> String {
69
        self.client
70
            .oauth()
71
            .client_id()
72
            .expect("OAuth client was registered before authorization")
73
            .as_str()
74
            .to_owned()
75
    }
76
77
    #[must_use]
78
    pub const fn is_pending(&self) -> bool {
79
        self.state.is_some()
80
    }
81
82
    pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
83
        let callback_url = Url::parse(callback_url)
84
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
85
        let state = self
86
            .state
87
            .take()
88
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
89
        let oauth = self.client.oauth();
90
        if let Err(error) = oauth.finish_login(callback_url.into()).await {
91
            oauth.abort_login(&state).await;
92
            return Err(account_error("OAuth login", error));
93
        }
94
        Session::from_client(self.client.clone())
95
    }
96
97
    pub async fn abort(mut self) {
98
        if let Some(state) = self.state.take() {
99
            self.client.oauth().abort_login(&state).await;
100
        }
101
    }
102
}
103
104
pub struct StoreConfig {
105
    path: PathBuf,
106
    passphrase: String,
107
}
108
109
impl StoreConfig {
110
    #[must_use]
111
    pub fn new(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
112
        Self {
113
            path: path.into(),
114
            passphrase: passphrase.into(),
115
        }
116
    }
117
118
    #[must_use]
119
    pub fn path(&self) -> &Path {
120
        &self.path
121
    }
122
}
123
124
/// Serializable authentication secrets. Store this separately from the
125
/// encrypted SQLite store in protected, preferably encrypted storage.
126
/// A saved record must be replaced when `Event::SessionTokensChanged` arrives.
127
#[derive(Clone, Deserialize, Serialize)]
128
pub struct SessionRecord {
129
    homeserver: String,
130
    authentication: AuthenticationRecord,
131
}
132
133
#[derive(Clone, Deserialize, Serialize)]
134
#[serde(tag = "type", content = "session", rename_all = "snake_case")]
135
enum AuthenticationRecord {
136
    Matrix(MatrixSession),
137
    OAuth {
138
        client_id: ClientId,
139
        user: UserSession,
140
    },
141
}
142
143
impl SessionRecord {
144
    #[must_use]
145
    pub fn homeserver(&self) -> &str {
146
        &self.homeserver
147
    }
148
}
149
150
#[derive(Debug, Error)]
151
pub enum AccountError {
152
    #[error("{context} failed")]
153
    Operation {
154
        context: &'static str,
155
        #[source]
156
        source: Box<dyn std::error::Error + Send + Sync>,
157
    },
158
    #[error("the homeserver returned no authenticated session")]
159
    MissingSession,
160
    #[error("the SDK returned an unsupported authentication session")]
161
    UnsupportedAuthentication,
162
    #[error("the OAuth callback was already submitted")]
163
    OAuthCallbackAlreadySubmitted,
164
    #[error(transparent)]
165
    Engine(#[from] EngineError),
166
}
167
168
impl Session {
169
    pub async fn login_password(
170
        login: PasswordLogin,
171
        store: StoreConfig,
172
    ) -> Result<Self, AccountError> {
173
        let client = matrix_client_builder(&store)
174
            .server_name_or_homeserver_url(login.homeserver.trim())
175
            .build()
176
            .await
177
            .map_err(|error| account_error("building the Matrix client", error))?;
178
        let mut request = client
179
            .matrix_auth()
180
            .login_username(login.username.trim(), &login.password)
181
            .request_refresh_token();
182
        if let Some(name) = login.device_display_name.as_deref() {
183
            request = request.initial_device_display_name(name);
184
        }
185
        if let Some(device_id) = login.device_id.as_deref() {
186
            request = request.device_id(device_id.as_str());
187
        }
188
        request
189
            .send()
190
            .await
191
            .map_err(|error| account_error("password login", error))?;
192
        Self::from_client(client)
193
    }
194
195
    pub async fn begin_oauth(
196
        login: OAuthLogin,
197
        store: StoreConfig,
198
    ) -> Result<PendingOAuthLogin, AccountError> {
199
        let redirect_uri = Url::parse(login.redirect_uri.trim())
200
            .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
201
        let client_uri = Url::parse(login.client_uri.trim())
202
            .map_err(|error| account_error("parsing the OAuth client URI", error))?;
203
        let client = matrix_client_builder(&store)
204
            .server_name_or_homeserver_url(login.homeserver.trim())
205
            .build()
206
            .await
207
            .map_err(|error| account_error("building the Matrix client", error))?;
208
        let application_type = match login.application_type {
209
            OAuthApplicationType::Native => ApplicationType::Native,
210
            OAuthApplicationType::Web => ApplicationType::Web,
211
        };
212
        let mut metadata = ClientMetadata::new(
213
            application_type,
214
            vec![OAuthGrantType::AuthorizationCode {
215
                redirect_uris: vec![redirect_uri.clone()],
216
            }],
217
            Localized::new(client_uri, []),
218
        );
219
        metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
220
        let metadata = Raw::new(&metadata)
221
            .map_err(|error| account_error("serializing OAuth client metadata", error))?;
222
        let mut registration = ClientRegistrationData::new(metadata);
223
        if let Some(client_id) = login.client_id {
224
            registration.static_registrations = Some(HashMap::from([(
225
                client.homeserver(),
226
                ClientId::new(client_id),
227
            )]));
228
        }
229
        let authorization = client
230
            .oauth()
231
            .login(redirect_uri, login.device_id, Some(registration), None)
232
            .build()
233
            .await
234
            .map_err(|error| account_error("starting OAuth login", error))?;
235
        Ok(PendingOAuthLogin {
236
            client,
237
            authorization_url: authorization.url.to_string(),
238
            state: Some(authorization.state),
239
        })
240
    }
241
242
    /// Restore a saved record with the same store location and passphrase.
243
    /// The caller must also handle refresh, rejection, and explicit logout.
244
    pub async fn restore(record: SessionRecord, store: StoreConfig) -> Result<Self, AccountError> {
245
        let client = matrix_client_builder(&store)
246
            .homeserver_url(&record.homeserver)
247
            .build()
248
            .await
249
            .map_err(|error| account_error("reopening the Matrix store", error))?;
250
        let authentication = match record.authentication.clone() {
251
            AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
252
            AuthenticationRecord::OAuth { client_id, user } => {
253
                OAuthSession { client_id, user }.into()
254
            }
255
        };
256
        client
257
            .restore_session(authentication)
258
            .await
259
            .map_err(|error| account_error("restoring the authenticated session", error))?;
260
        Ok(Self { client, record })
261
    }
262
263
    /// Initial login credentials only. This snapshot does not track later
264
    /// token rotations; use `ClientHandle::session_record` after building.
265
    #[must_use]
266
    pub fn record(&self) -> SessionRecord {
267
        self.record.clone()
268
    }
269
270
    pub async fn build(
271
        self,
272
        config: EngineConfig,
273
    ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
274
        Engine::from_client(self.client, config)
275
            .await
276
            .map_err(AccountError::from)
277
    }
278
279
    fn from_client(client: Client) -> Result<Self, AccountError> {
280
        let record = session_record(&client)?;
281
        Ok(Self { client, record })
282
    }
283
}
284
285
pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
286
    let authentication = match client.session().ok_or(AccountError::MissingSession)? {
287
        AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
288
        AuthSession::OAuth(session) => {
289
            let OAuthSession { client_id, user } = *session;
290
            AuthenticationRecord::OAuth { client_id, user }
291
        }
292
        _ => return Err(AccountError::UnsupportedAuthentication),
293
    };
294
    Ok(SessionRecord {
295
        homeserver: client.homeserver().to_string(),
296
        authentication,
297
    })
298
}
299
300
fn encryption_settings() -> EncryptionSettings {
301
    EncryptionSettings {
302
        auto_enable_cross_signing: true,
303
        backup_download_strategy: BackupDownloadStrategy::OneShot,
304
        auto_enable_backups: true,
305
    }
306
}
307
308
fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
309
    Client::builder()
310
        .with_encryption_settings(encryption_settings())
311
        .with_threading_support(ThreadingSupport::Enabled {
312
            with_subscriptions: false,
313
        })
314
        .handle_refresh_tokens()
315
        .sqlite_store(&store.path, Some(&store.passphrase))
316
}
317
318
fn account_error(
319
    context: &'static str,
320
    source: impl std::error::Error + Send + Sync + 'static,
321
) -> AccountError {
322
    AccountError::Operation {
323
        context,
324
        source: Box::new(source),
325
    }
326
}