From 6657e073a02b0e1c577b9608c336ddd0d5bf728d Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Thu, 10 Dec 2020 12:49:00 +0100 Subject: [PATCH] refactor: Follow up clean up bootstrap --- lib/encryption/utils/bootstrap.dart | 54 +++++-- lib/matrix_api.dart | 10 ++ lib/matrix_api/matrix_api.dart | 50 +++--- .../model/auth/authentication_data.dart | 36 +++++ .../model/auth/authentication_identifier.dart | 33 ++++ .../model/auth/authentication_password.dart | 85 ++++++++++ .../auth/authentication_phone_identifier.dart | 42 +++++ .../model/auth/authentication_recaptcha.dart | 42 +++++ ...authentication_third_party_identifier.dart | 42 +++++ .../auth/authentication_three_pid_creds.dart | 85 ++++++++++ .../model/auth/authentication_token.dart | 45 ++++++ .../model/auth/authentication_types.dart | 34 ++++ .../auth/authentication_user_identifier.dart | 39 +++++ lib/src/client.dart | 45 +++--- lib/src/utils/uia_request.dart | 59 ++++--- test/matrix_api_test.dart | 153 ++++++++++++++++-- test/uia_test.dart | 35 ++-- 17 files changed, 783 insertions(+), 106 deletions(-) create mode 100644 lib/matrix_api/model/auth/authentication_data.dart create mode 100644 lib/matrix_api/model/auth/authentication_identifier.dart create mode 100644 lib/matrix_api/model/auth/authentication_password.dart create mode 100644 lib/matrix_api/model/auth/authentication_phone_identifier.dart create mode 100644 lib/matrix_api/model/auth/authentication_recaptcha.dart create mode 100644 lib/matrix_api/model/auth/authentication_third_party_identifier.dart create mode 100644 lib/matrix_api/model/auth/authentication_three_pid_creds.dart create mode 100644 lib/matrix_api/model/auth/authentication_token.dart create mode 100644 lib/matrix_api/model/auth/authentication_types.dart create mode 100644 lib/matrix_api/model/auth/authentication_user_identifier.dart diff --git a/lib/encryption/utils/bootstrap.dart b/lib/encryption/utils/bootstrap.dart index af5a7e91..4c454184 100644 --- a/lib/encryption/utils/bootstrap.dart +++ b/lib/encryption/utils/bootstrap.dart @@ -29,19 +29,44 @@ import '../../famedlysdk.dart'; import '../../matrix_api/utils/logs.dart'; enum BootstrapState { - loading, // loading - askWipeSsss, // existing SSSS found, should we wipe it? - askUseExistingSsss, // ask if an existing SSSS should be userDeviceKeys - askBadSsss, // SSSS is in a bad state, continue with potential dataloss? - askUnlockSsss, // Ask to unlock all the SSSS keys - askNewSsss, // Ask for new SSSS key / passphrase - openExistingSsss, // Open an existing SSSS key - askWipeCrossSigning, // Ask if cross signing should be wiped - askSetupCrossSigning, // Ask if cross signing should be set up - askWipeOnlineKeyBackup, // Ask if online key backup should be wiped - askSetupOnlineKeyBackup, // Ask if the online key backup should be set up - error, // error - done, // done + /// Is loading. + loading, + + /// Existing SSSS found, should we wipe it? + askWipeSsss, + + /// Ask if an existing SSSS should be userDeviceKeys + askUseExistingSsss, + + /// Ask to unlock all the SSSS keys + askUnlockSsss, + + /// SSSS is in a bad state, continue with potential dataloss? + askBadSsss, + + /// Ask for new SSSS key / passphrase + askNewSsss, + + /// Open an existing SSSS key + openExistingSsss, + + /// Ask if cross signing should be wiped + askWipeCrossSigning, + + /// Ask if cross signing should be set up + askSetupCrossSigning, + + /// Ask if online key backup should be wiped + askWipeOnlineKeyBackup, + + /// Ask if the online key backup should be set up + askSetupOnlineKeyBackup, + + /// An error has been occured. + error, + + /// done + done, } /// Bootstrapping SSSS and cross-signing @@ -425,8 +450,9 @@ class Bootstrap { } try { // upload the keys! + state = BootstrapState.loading; await client.uiaRequestBackground( - (Map auth) => client.uploadDeviceSigningKeys( + (AuthenticationData auth) => client.uploadDeviceSigningKeys( masterKey: masterKey, selfSigningKey: selfSigningKey, userSigningKey: userSigningKey, diff --git a/lib/matrix_api.dart b/lib/matrix_api.dart index f4fe3005..669dd917 100644 --- a/lib/matrix_api.dart +++ b/lib/matrix_api.dart @@ -66,6 +66,16 @@ export 'matrix_api/model/upload_key_signatures_response.dart'; export 'matrix_api/model/user_search_result.dart'; export 'matrix_api/model/well_known_informations.dart'; export 'matrix_api/model/who_is_info.dart'; +export 'matrix_api/model/auth/authentication_data.dart'; +export 'matrix_api/model/auth/authentication_identifier.dart'; +export 'matrix_api/model/auth/authentication_password.dart'; +export 'matrix_api/model/auth/authentication_phone_identifier.dart'; +export 'matrix_api/model/auth/authentication_recaptcha.dart'; +export 'matrix_api/model/auth/authentication_third_party_identifier.dart'; +export 'matrix_api/model/auth/authentication_three_pid_creds.dart'; +export 'matrix_api/model/auth/authentication_token.dart'; +export 'matrix_api/model/auth/authentication_types.dart'; +export 'matrix_api/model/auth/authentication_user_identifier.dart'; export 'matrix_api/model/events/secret_storage_default_key_content.dart'; export 'matrix_api/model/events/secret_storage_key_content.dart'; export 'matrix_api/model/events/tombstone_content.dart'; diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index a33f3e7f..cb2929ce 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -19,10 +19,12 @@ import 'dart:async'; import 'dart:convert'; +import 'package:famedlysdk/famedlysdk.dart'; import 'package:http/http.dart' as http; import 'package:mime/mime.dart'; import 'package:moor/moor.dart'; +import 'model/auth/authentication_types.dart'; import 'model/device.dart'; import 'model/event_context.dart'; import 'model/events_sync_update.dart'; @@ -247,7 +249,7 @@ class MatrixApi { /// Authenticates the user, and issues an access token they can use to authorize themself in subsequent requests. /// https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-login Future login({ - String type = 'm.login.password', + String type = AuthenticationTypes.password, String userIdentifierType, String user, String medium, @@ -256,7 +258,7 @@ class MatrixApi { String token, String deviceId, String initialDeviceDisplayName, - Map auth, + AuthenticationData auth, }) async { final response = await request(RequestType.POST, '/client/r0/login', data: { 'type': type, @@ -272,7 +274,7 @@ class MatrixApi { if (deviceId != null) 'device_id': deviceId, if (initialDeviceDisplayName != null) 'initial_device_display_name': initialDeviceDisplayName, - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return LoginResponse.fromJson(response); } @@ -302,13 +304,25 @@ class MatrixApi { return; } + /// Register for an account on this homeserver. + /// + /// There are two kinds of user account: + /// + /// user accounts. These accounts may use the full API described in this + /// specification. + /// guest accounts. These accounts may have limited permissions and may not + /// be supported by all servers. + /// + /// If registration is successful, this endpoint will issue an access token + /// the client can use to authorize itself in subsequent requests. + /// https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register Future register({ String username, String password, String deviceId, String initialDeviceDisplayName, bool inhibitLogin, - Map auth, + AuthenticationData auth, String kind, }) async { var action = '/client/r0/register'; @@ -320,7 +334,7 @@ class MatrixApi { if (initialDeviceDisplayName != null) 'initial_device_display_name': initialDeviceDisplayName, if (inhibitLogin != null) 'inhibit_login': inhibitLogin, - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return LoginResponse.fromJson(response); } @@ -382,11 +396,11 @@ class MatrixApi { /// https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-password Future changePassword( String newPassword, { - Map auth, + AuthenticationData auth, }) async { await request(RequestType.POST, '/client/r0/account/password', data: { 'new_password': newPassword, - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return; } @@ -443,14 +457,15 @@ class MatrixApi { return RequestTokenResponse.fromJson(response); } + /// https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-deactivate Future deactivateAccount({ String idServer, - Map auth, + AuthenticationData auth, }) async { final response = await request(RequestType.POST, '/client/r0/account/deactivate', data: { if (idServer != null) 'id_server': idServer, - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return IdServerUnbindResult.values.firstWhere( @@ -486,12 +501,12 @@ class MatrixApi { Future addThirdPartyIdentifier( String clientSecret, String sid, { - Map auth, + AuthenticationData auth, }) async { await request(RequestType.POST, '/client/r0/account/3pid/add', data: { 'sid': sid, 'client_secret': clientSecret, - if (auth != null && auth.isNotEmpty) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return; } @@ -1381,12 +1396,11 @@ class MatrixApi { /// Deletes the given device, and invalidates any access token associated with it. /// https://matrix.org/docs/spec/client_server/r0.6.1#delete-matrix-client-r0-devices-deviceid - Future deleteDevice(String deviceId, - {Map auth}) async { + Future deleteDevice(String deviceId, {AuthenticationData auth}) async { await request(RequestType.DELETE, '/client/r0/devices/${Uri.encodeComponent(deviceId)}', data: { - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return; } @@ -1394,10 +1408,10 @@ class MatrixApi { /// Deletes the given devices, and invalidates any access token associated with them. /// https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-delete-devices Future deleteDevices(List deviceIds, - {Map auth}) async { + {AuthenticationData auth}) async { await request(RequestType.POST, '/client/r0/delete_devices', data: { 'devices': deviceIds, - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }); return; } @@ -1470,7 +1484,7 @@ class MatrixApi { MatrixCrossSigningKey masterKey, MatrixCrossSigningKey selfSigningKey, MatrixCrossSigningKey userSigningKey, - Map auth, + AuthenticationData auth, }) async { await request( RequestType.POST, @@ -1479,7 +1493,7 @@ class MatrixApi { if (masterKey != null) 'master_key': masterKey.toJson(), if (selfSigningKey != null) 'self_signing_key': selfSigningKey.toJson(), if (userSigningKey != null) 'user_signing_key': userSigningKey.toJson(), - if (auth != null) 'auth': auth, + if (auth != null) 'auth': auth.toJson(), }, ); } diff --git a/lib/matrix_api/model/auth/authentication_data.dart b/lib/matrix_api/model/auth/authentication_data.dart new file mode 100644 index 00000000..248ad27e --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_data.dart @@ -0,0 +1,36 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class AuthenticationData { + String type; + String session; + + AuthenticationData({this.type, this.session}); + + AuthenticationData.fromJson(Map json) { + type = json['type']; + session = json['session']; + } + + Map toJson() { + final data = {}; + data['type'] = type; + data['session'] = session; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_identifier.dart b/lib/matrix_api/model/auth/authentication_identifier.dart new file mode 100644 index 00000000..be5a4bfa --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_identifier.dart @@ -0,0 +1,33 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class AuthenticationIdentifier { + String type; + + AuthenticationIdentifier({this.type}); + + AuthenticationIdentifier.fromJson(Map json) { + type = json['type']; + } + + Map toJson() { + final data = {}; + data['type'] = type; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_password.dart b/lib/matrix_api/model/auth/authentication_password.dart new file mode 100644 index 00000000..0a95a7ba --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_password.dart @@ -0,0 +1,85 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:famedlysdk/matrix_api/model/auth/authentication_user_identifier.dart'; + +import 'authentication_data.dart'; +import 'authentication_identifier.dart'; +import 'authentication_phone_identifier.dart'; +import 'authentication_third_party_identifier.dart'; +import 'authentication_types.dart'; + +class AuthenticationPassword extends AuthenticationData { + String user; + String password; + + /// You may want to cast this as [AuthenticationUserIdentifier] or other + /// Identifier classes extending AuthenticationIdentifier. + AuthenticationIdentifier identifier; + + AuthenticationPassword( + {String session, this.password, this.user, this.identifier}) + : super( + type: AuthenticationTypes.password, + session: session, + ); + + AuthenticationPassword.fromJson(Map json) + : super.fromJson(json) { + user = json['user']; + password = json['password']; + identifier = AuthenticationIdentifier.fromJson(json['identifier']); + switch (identifier.type) { + case AuthenticationIdentifierTypes.userId: + identifier = AuthenticationUserIdentifier.fromJson(json['identifier']); + break; + case AuthenticationIdentifierTypes.phone: + identifier = AuthenticationPhoneIdentifier.fromJson(json['identifier']); + break; + case AuthenticationIdentifierTypes.thirdParty: + identifier = + AuthenticationThirdPartyIdentifier.fromJson(json['identifier']); + break; + } + } + + @override + Map toJson() { + final data = super.toJson(); + if (user != null) data['user'] = user; + data['password'] = password; + switch (identifier.type) { + case AuthenticationIdentifierTypes.userId: + data['identifier'] = + (identifier as AuthenticationUserIdentifier).toJson(); + break; + case AuthenticationIdentifierTypes.phone: + data['identifier'] = + (identifier as AuthenticationPhoneIdentifier).toJson(); + break; + case AuthenticationIdentifierTypes.thirdParty: + data['identifier'] = + (identifier as AuthenticationThirdPartyIdentifier).toJson(); + break; + default: + data['identifier'] = identifier.toJson(); + break; + } + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_phone_identifier.dart b/lib/matrix_api/model/auth/authentication_phone_identifier.dart new file mode 100644 index 00000000..452f01c2 --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_phone_identifier.dart @@ -0,0 +1,42 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_identifier.dart'; +import 'authentication_types.dart'; + +class AuthenticationPhoneIdentifier extends AuthenticationIdentifier { + String country; + String phone; + + AuthenticationPhoneIdentifier({this.country, this.phone}) + : super(type: AuthenticationIdentifierTypes.phone); + + AuthenticationPhoneIdentifier.fromJson(Map json) + : super.fromJson(json) { + country = json['country']; + phone = json['phone']; + } + + @override + Map toJson() { + final data = super.toJson(); + data['country'] = country; + data['phone'] = phone; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_recaptcha.dart b/lib/matrix_api/model/auth/authentication_recaptcha.dart new file mode 100644 index 00000000..3d0816ad --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_recaptcha.dart @@ -0,0 +1,42 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_data.dart'; +import 'authentication_types.dart'; + +class AuthenticationRecaptcha extends AuthenticationData { + String response; + + AuthenticationRecaptcha({String session, this.response}) + : super( + type: AuthenticationTypes.recaptcha, + session: session, + ); + + AuthenticationRecaptcha.fromJson(Map json) + : super.fromJson(json) { + response = json['response']; + } + + @override + Map toJson() { + final data = super.toJson(); + data['response'] = response; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_third_party_identifier.dart b/lib/matrix_api/model/auth/authentication_third_party_identifier.dart new file mode 100644 index 00000000..5a4ab496 --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_third_party_identifier.dart @@ -0,0 +1,42 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_identifier.dart'; +import 'authentication_types.dart'; + +class AuthenticationThirdPartyIdentifier extends AuthenticationIdentifier { + String medium; + String address; + + AuthenticationThirdPartyIdentifier({this.medium, this.address}) + : super(type: AuthenticationIdentifierTypes.thirdParty); + + AuthenticationThirdPartyIdentifier.fromJson(Map json) + : super.fromJson(json) { + medium = json['medium']; + address = json['address']; + } + + @override + Map toJson() { + final data = super.toJson(); + data['medium'] = medium; + data['address'] = address; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_three_pid_creds.dart b/lib/matrix_api/model/auth/authentication_three_pid_creds.dart new file mode 100644 index 00000000..1ddb45e7 --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_three_pid_creds.dart @@ -0,0 +1,85 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_data.dart'; + +/// For email based identity: +/// https://matrix.org/docs/spec/client_server/r0.6.1#email-based-identity-homeserver +/// Or phone number based identity: +/// https://matrix.org/docs/spec/client_server/r0.6.1#phone-number-msisdn-based-identity-homeserver +class AuthenticationThreePidCreds extends AuthenticationData { + List threepidCreds; + + AuthenticationThreePidCreds({String session, String type, this.threepidCreds}) + : super( + type: type, + session: session, + ); + + AuthenticationThreePidCreds.fromJson(Map json) + : super.fromJson(json) { + if (json['threepidCreds'] != null) { + threepidCreds = (json['threepidCreds'] as List) + .map((item) => ThreepidCreds.fromJson(item)) + .toList(); + } + + // This is so extremly stupid... kill it with fire! + if (json['threepid_creds'] != null) { + threepidCreds = (json['threepid_creds'] as List) + .map((item) => ThreepidCreds.fromJson(item)) + .toList(); + } + } + + @override + Map toJson() { + final data = super.toJson(); + data['threepidCreds'] = threepidCreds.map((t) => t.toJson()).toList(); + // Help me! I'm prisoned in a developer factory against my will, + // where we are forced to work with json like this!! + data['threepid_creds'] = threepidCreds.map((t) => t.toJson()).toList(); + return data; + } +} + +class ThreepidCreds { + String sid; + String clientSecret; + String idServer; + String idAccessToken; + + ThreepidCreds( + {this.sid, this.clientSecret, this.idServer, this.idAccessToken}); + + ThreepidCreds.fromJson(Map json) { + sid = json['sid']; + clientSecret = json['client_secret']; + idServer = json['id_server']; + idAccessToken = json['id_access_token']; + } + + Map toJson() { + final data = {}; + data['sid'] = sid; + data['client_secret'] = clientSecret; + data['id_server'] = idServer; + data['id_access_token'] = idAccessToken; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_token.dart b/lib/matrix_api/model/auth/authentication_token.dart new file mode 100644 index 00000000..23db3755 --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_token.dart @@ -0,0 +1,45 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_data.dart'; +import 'authentication_types.dart'; + +class AuthenticationToken extends AuthenticationData { + String token; + String txnId; + + AuthenticationToken({String session, this.token, this.txnId}) + : super( + type: AuthenticationTypes.token, + session: session, + ); + + AuthenticationToken.fromJson(Map json) + : super.fromJson(json) { + token = json['token']; + txnId = json['txn_id']; + } + + @override + Map toJson() { + final data = super.toJson(); + data['token'] = token; + data['txn_id'] = txnId; + return data; + } +} diff --git a/lib/matrix_api/model/auth/authentication_types.dart b/lib/matrix_api/model/auth/authentication_types.dart new file mode 100644 index 00000000..34e357f3 --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_types.dart @@ -0,0 +1,34 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +abstract class AuthenticationTypes { + static const String password = 'm.login.password'; + static const String recaptcha = 'm.login.recaptcha'; + static const String token = 'm.login.token'; + static const String oauth2 = 'm.login.oauth2'; + static const String sso = 'm.login.sso'; + static const String emailIdentity = 'm.login.email.identity'; + static const String msisdn = 'm.login.msisdn'; + static const String dummy = 'm.login.dummy'; +} + +abstract class AuthenticationIdentifierTypes { + static const String userId = 'm.id.user'; + static const String thirdParty = 'm.id.thirdparty'; + static const String phone = 'm.id.phone'; +} diff --git a/lib/matrix_api/model/auth/authentication_user_identifier.dart b/lib/matrix_api/model/auth/authentication_user_identifier.dart new file mode 100644 index 00000000..66e6091e --- /dev/null +++ b/lib/matrix_api/model/auth/authentication_user_identifier.dart @@ -0,0 +1,39 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'authentication_identifier.dart'; +import 'authentication_types.dart'; + +class AuthenticationUserIdentifier extends AuthenticationIdentifier { + String user; + + AuthenticationUserIdentifier({this.user}) + : super(type: AuthenticationIdentifierTypes.userId); + + AuthenticationUserIdentifier.fromJson(Map json) + : super.fromJson(json) { + user = json['user']; + } + + @override + Map toJson() { + final data = super.toJson(); + data['user'] = user; + return data; + } +} diff --git a/lib/src/client.dart b/lib/src/client.dart index 8bd4292d..0cfdaeec 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -309,7 +309,7 @@ class Client extends MatrixApi { String deviceId, String initialDeviceDisplayName, bool inhibitLogin, - Map auth, + AuthenticationData auth, String kind, }) async { final response = await super.register( @@ -342,8 +342,8 @@ class Client extends MatrixApi { /// You have to call [checkHomeserver] first to set a homeserver. @override Future login({ - String type = 'm.login.password', - String userIdentifierType = 'm.id.user', + String type = AuthenticationTypes.password, + String userIdentifierType = AuthenticationIdentifierTypes.userId, String user, String medium, String address, @@ -351,7 +351,7 @@ class Client extends MatrixApi { String token, String deviceId, String initialDeviceDisplayName, - Map auth, + AuthenticationData auth, }) async { final loginResp = await super.login( type: type, @@ -412,15 +412,15 @@ class Client extends MatrixApi { /// Run any request and react on user interactive authentication flows here. Future uiaRequestBackground( - Future Function(Map auth) request) { + Future Function(AuthenticationData auth) request) { final completer = Completer(); UiaRequest uia; uia = UiaRequest( request: request, - onDone: () { - if (uia.done) { + onUpdate: (state) { + if (state == UiaRequestState.done) { completer.complete(uia.result); - } else if (uia.fail) { + } else if (state == UiaRequestState.fail) { completer.completeError(uia.error); } }, @@ -538,7 +538,7 @@ class Client extends MatrixApi { : null; static const Set supportedVersions = {'r0.5.0', 'r0.6.0'}; - static const Set supportedLoginTypes = {'m.login.password'}; + static const Set supportedLoginTypes = {AuthenticationTypes.password}; static const String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}'; static const String messagesFilters = '{"lazy_load_members":true}'; @@ -1603,14 +1603,14 @@ sort order of ${prevState.sortOrder}. This should never happen...'''); /// Changes the password. You should either set oldPasswort or another authentication flow. @override Future changePassword(String newPassword, - {String oldPassword, Map auth}) async { + {String oldPassword, AuthenticationData auth}) async { try { if (oldPassword != null) { - auth = { - 'type': 'm.login.password', - 'user': userID, - 'password': oldPassword, - }; + auth = AuthenticationPassword( + user: userID, + identifier: AuthenticationUserIdentifier(user: userID), + password: oldPassword, + ); } await super.changePassword(newPassword, auth: auth); } on MatrixException catch (matrixException) { @@ -1619,7 +1619,7 @@ sort order of ${prevState.sortOrder}. This should never happen...'''); } if (matrixException.authenticationFlows.length != 1 || !matrixException.authenticationFlows.first.stages - .contains('m.login.password')) { + .contains(AuthenticationTypes.password)) { rethrow; } if (oldPassword == null) { @@ -1627,13 +1627,12 @@ sort order of ${prevState.sortOrder}. This should never happen...'''); } return changePassword( newPassword, - auth: { - 'type': 'm.login.password', - 'user': userID, - 'identifier': {'type': 'm.id.user', 'user': userID}, - 'password': oldPassword, - 'session': matrixException.session, - }, + auth: AuthenticationPassword( + user: userID, + identifier: AuthenticationUserIdentifier(user: userID), + password: oldPassword, + session: matrixException.session, + ), ); } catch (_) { rethrow; diff --git a/lib/src/utils/uia_request.dart b/lib/src/utils/uia_request.dart index 9a7011f3..2aac78b5 100644 --- a/lib/src/utils/uia_request.dart +++ b/lib/src/utils/uia_request.dart @@ -18,30 +18,48 @@ import '../../famedlysdk.dart'; +enum UiaRequestState { + /// The request is done + done, + + /// The request has failed + fail, + + /// The request is currently loading + loading, + + /// The request is waiting for user interaction + waitForUser, +} + /// Wrapper to handle User interactive authentication requests class UiaRequest { - void Function() onUpdate; - void Function() onDone; - final Future Function(Map auth) request; + void Function(UiaRequestState state) onUpdate; + final Future Function(AuthenticationData auth) request; String session; - bool done = false; - bool fail = false; + UiaRequestState _state = UiaRequestState.loading; T result; Exception error; Set nextStages = {}; Map params = {}; - UiaRequest({this.onUpdate, this.request, this.onDone}) { - run(); + + UiaRequestState get state => _state; + set state(UiaRequestState newState) { + if (_state == newState) return; + _state = newState; + onUpdate?.call(newState); } - Future run([Map auth]) async { + UiaRequest({this.onUpdate, this.request}) { + _run(); + } + + Future _run([AuthenticationData auth]) async { + state = UiaRequestState.loading; try { - auth ??= {}; - if (session != null) { - auth['session'] = session; - } + auth ??= AuthenticationData(session: session); final res = await request(auth); - done = true; + state = UiaRequestState.done; result = res; return res; } on MatrixException catch (err) { @@ -59,23 +77,16 @@ class UiaRequest { return null; } catch (err) { error = err is Exception ? err : Exception(err); - fail = true; + state = UiaRequestState.fail; return null; } finally { - if (onUpdate != null) { - onUpdate(); - } - if ((fail || done) && onDone != null) { - onDone(); + if (state == UiaRequestState.loading) { + state = UiaRequestState.waitForUser; } } } - Future completeStage(String type, [Map auth]) async { - auth ??= {}; - auth['type'] = type; - return await run(auth); - } + Future completeStage(AuthenticationData auth) => _run(auth); Set getNextStages( List flows, List completed) { diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index c9dab61b..bd32927d 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -204,11 +204,14 @@ void main() { test('changePassword', () async { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; - await matrixApi.changePassword('1234', auth: { - 'type': 'example.type.foo', - 'session': 'xxxxx', - 'example_credential': 'verypoorsharedsecret' - }); + await matrixApi.changePassword( + '1234', + auth: AuthenticationData.fromJson({ + 'type': 'example.type.foo', + 'session': 'xxxxx', + 'example_credential': 'verypoorsharedsecret' + }), + ); matrixApi.homeserver = matrixApi.accessToken = null; }); test('resetPasswordUsingEmail', () async { @@ -241,12 +244,14 @@ void main() { test('deactivateAccount', () async { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; - final response = await matrixApi - .deactivateAccount(idServer: 'https://example.com', auth: { - 'type': 'example.type.foo', - 'session': 'xxxxx', - 'example_credential': 'verypoorsharedsecret' - }); + final response = await matrixApi.deactivateAccount( + idServer: 'https://example.com', + auth: AuthenticationData.fromJson({ + 'type': 'example.type.foo', + 'session': 'xxxxx', + 'example_credential': 'verypoorsharedsecret' + }), + ); expect(response, IdServerUnbindResult.success); matrixApi.homeserver = matrixApi.accessToken = null; }); @@ -267,7 +272,8 @@ void main() { test('addThirdPartyIdentifier', () async { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; - await matrixApi.addThirdPartyIdentifier('1234', '1234', auth: {}); + await matrixApi.addThirdPartyIdentifier('1234', '1234', + auth: AuthenticationData.fromJson({'type': 'm.login.dummy'})); matrixApi.homeserver = matrixApi.accessToken = null; }); test('bindThirdPartyIdentifier', () async { @@ -1041,7 +1047,8 @@ void main() { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; - await matrixApi.deleteDevice('QBUAZIFURK', auth: {}); + await matrixApi.deleteDevice('QBUAZIFURK', + auth: AuthenticationData.fromJson({})); matrixApi.homeserver = matrixApi.accessToken = null; }); @@ -1049,7 +1056,8 @@ void main() { matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting'); matrixApi.accessToken = '1234'; - await matrixApi.deleteDevices(['QBUAZIFURK'], auth: {}); + await matrixApi + .deleteDevices(['QBUAZIFURK'], auth: AuthenticationData.fromJson({})); matrixApi.homeserver = matrixApi.accessToken = null; }); @@ -1785,5 +1793,122 @@ void main() { ['/client/unstable/room_keys/keys?version=5']({}), ret.toJson()); }); + test('AuthenticationData', () { + final json = {'session': '1234', 'type': 'm.login.dummy'}; + expect(AuthenticationData.fromJson(json).toJson(), json); + expect( + AuthenticationData(session: '1234', type: 'm.login.dummy').toJson(), + json); + }); + test('AuthenticationRecaptcha', () { + final json = { + 'session': '1234', + 'type': 'm.login.recaptcha', + 'response': 'a', + }; + expect(AuthenticationRecaptcha.fromJson(json).toJson(), json); + expect(AuthenticationRecaptcha(session: '1234', response: 'a').toJson(), + json); + }); + test('AuthenticationToken', () { + final json = { + 'session': '1234', + 'type': 'm.login.token', + 'token': 'a', + 'txn_id': '1' + }; + expect(AuthenticationToken.fromJson(json).toJson(), json); + expect( + AuthenticationToken(session: '1234', token: 'a', txnId: '1').toJson(), + json); + }); + test('AuthenticationThreePidCreds', () { + final json = { + 'type': 'm.login.email.identity', + 'threepidCreds': [ + { + 'sid': '1', + 'client_secret': 'a', + 'id_server': 'matrix.org', + 'id_access_token': 'a', + }, + ], + 'threepid_creds': [ + { + 'sid': '1', + 'client_secret': 'a', + 'id_server': 'matrix.org', + 'id_access_token': 'a', + }, + ], + 'session': '1', + }; + expect(AuthenticationThreePidCreds.fromJson(json).toJson(), json); + expect( + AuthenticationThreePidCreds( + session: '1', + type: AuthenticationTypes.emailIdentity, + threepidCreds: [ + ThreepidCreds( + sid: '1', + clientSecret: 'a', + idServer: 'matrix.org', + idAccessToken: 'a', + ), + ]).toJson(), + json); + }); + test('AuthenticationIdentifier', () { + final json = {'type': 'm.id.user'}; + expect(AuthenticationIdentifier.fromJson(json).toJson(), json); + expect(AuthenticationIdentifier(type: 'm.id.user').toJson(), json); + }); + test('AuthenticationPassword', () { + final json = { + 'type': 'm.login.password', + 'identifier': {'type': 'm.id.user', 'user': 'a'}, + 'password': 'a', + 'session': '1', + 'user': 'a', + }; + expect(AuthenticationPassword.fromJson(json).toJson(), json); + expect( + AuthenticationPassword( + session: '1', + password: 'a', + user: 'a', + identifier: AuthenticationUserIdentifier(user: 'a'), + ).toJson(), + json); + json['identifier'] = { + 'type': 'm.id.thirdparty', + 'medium': 'a', + 'address': 'a', + }; + expect(AuthenticationPassword.fromJson(json).toJson(), json); + expect( + AuthenticationPassword( + session: '1', + password: 'a', + user: 'a', + identifier: + AuthenticationThirdPartyIdentifier(medium: 'a', address: 'a'), + ).toJson(), + json); + json['identifier'] = { + 'type': 'm.id.phone', + 'country': 'a', + 'phone': 'a', + }; + expect(AuthenticationPassword.fromJson(json).toJson(), json); + expect( + AuthenticationPassword( + session: '1', + password: 'a', + user: 'a', + identifier: AuthenticationPhoneIdentifier(country: 'a', phone: 'a'), + ).toJson(), + json); + }); }); } diff --git a/test/uia_test.dart b/test/uia_test.dart index ab3b7b74..c49c9e59 100644 --- a/test/uia_test.dart +++ b/test/uia_test.dart @@ -29,14 +29,14 @@ void main() { var finished = false; final request = UiaRequest( request: (auth) async { - if (auth['session'] != null && auth['session'] != 'foxies') { + if (auth.session != null && auth.session != 'foxies') { throw MatrixException.fromJson({}); } - if (auth['type'] == 'stage1') { + if (auth.type == 'stage1') { if (completed.isEmpty) { completed.add('stage1'); } - } else if (auth['type'] == 'stage2') { + } else if (auth.type == 'stage2') { if (completed.length == 1 && completed[0] == 'stage1') { // okay, we are done! return 'FOXIES ARE FLOOOOOFY!!!!!'; @@ -54,24 +54,29 @@ void main() { }; throw MatrixException.fromJson(res); }, - onUpdate: () => updated++, - onDone: () => finished = true, + onUpdate: (state) { + if (state == UiaRequestState.done) { + finished = true; + } + updated++; + }, ); await Future.delayed(Duration(milliseconds: 50)); expect(request.nextStages.contains('stage1'), true); expect(request.nextStages.length, 1); expect(updated, 1); expect(finished, false); - await request.completeStage('stage1'); + await request.completeStage(AuthenticationData(type: 'stage1')); expect(request.nextStages.contains('stage2'), true); expect(request.nextStages.length, 1); - expect(updated, 2); + expect(updated, 3); expect(finished, false); - final res = await request.completeStage('stage2'); + final res = + await request.completeStage(AuthenticationData(type: 'stage2')); expect(res, 'FOXIES ARE FLOOOOOFY!!!!!'); expect(request.result, 'FOXIES ARE FLOOOOOFY!!!!!'); - expect(request.done, true); - expect(updated, 3); + expect(request.state, UiaRequestState.done); + expect(updated, 5); expect(finished, true); }); test('it should throw errors', () async { @@ -81,11 +86,15 @@ void main() { request: (auth) async { throw Exception('nope'); }, - onUpdate: () => updated = true, - onDone: () => finished = true, + onUpdate: (state) { + if (state == UiaRequestState.fail) { + finished = true; + } + updated = true; + }, ); await Future.delayed(Duration(milliseconds: 50)); - expect(request.fail, true); + expect(request.state, UiaRequestState.fail); expect(updated, true); expect(finished, true); expect(request.error.toString(), Exception('nope').toString());