atagen

macltok

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