diff --git a/lib/components/dialogs/bootstrap_dialog.dart b/lib/components/dialogs/bootstrap_dialog.dart new file mode 100644 index 00000000..f7018705 --- /dev/null +++ b/lib/components/dialogs/bootstrap_dialog.dart @@ -0,0 +1,294 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/encryption/utils/bootstrap.dart'; +import 'package:fluffychat/components/dialogs/adaptive_flat_button.dart'; +import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../matrix.dart'; + +class BootstrapDialog extends StatefulWidget { + Future show(BuildContext context) => PlatformInfos.isCupertinoStyle + ? showCupertinoDialog(context: context, builder: (context) => this) + : showDialog(context: context, builder: (context) => this); + + @override + _BootstrapDialogState createState() => _BootstrapDialogState(); +} + +class _BootstrapDialogState extends State { + Bootstrap bootstrap; + + @override + Widget build(BuildContext context) { + bootstrap ??= Matrix.of(context) + .client + .encryption + .bootstrap(onUpdate: () => setState(() => null)); + + final buttons = []; + Widget body; + + switch (bootstrap.state) { + case BootstrapState.loading: + body = LinearProgressIndicator(); + break; + case BootstrapState.askWipeSsss: + body = Text('Wipe chat backup?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.wipeSsss(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.wipeSsss(false), + )); + break; + case BootstrapState.askUseExistingSsss: + body = Text('Use existing chat backup?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.useExistingSsss(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.useExistingSsss(false), + )); + break; + case BootstrapState.askBadSsss: + body = Text('SSSS bad - continue nevertheless? DATALOSS!!!'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.ignoreBadSecrets(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.ignoreBadSecrets(false), + )); + break; + case BootstrapState.askUnlockSsss: + final widgets = [Text('Unlock old SSSS')]; + for (final entry in bootstrap.oldSsssKeys.entries) { + final keyId = entry.key; + final key = entry.value; + widgets.add(Flexible(child: _AskUnlockOldSsss(keyId, key))); + } + body = Column( + children: widgets, + mainAxisSize: MainAxisSize.min, + ); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).confirm), + onPressed: () => bootstrap.unlockedSsss(), + )); + break; + case BootstrapState.askNewSsss: + body = Text('Please set a long passphrase to secure your backup.'); + buttons.add(AdaptiveFlatButton( + child: Text('Enter a new passphrase'), + onPressed: () async { + final input = + await showTextInputDialog(context: context, textFields: [ + DialogTextField( + minLines: 1, + maxLines: 1, + obscureText: true, + ) + ]); + if (input?.isEmpty ?? true) return; + await bootstrap.newSsss(input.single); + })); + break; + case BootstrapState.openExistingSsss: + body = Text('Please enter your passphrase!'); + buttons.add(AdaptiveFlatButton( + child: Text('Enter passphrase'), + onPressed: () async { + final input = + await showTextInputDialog(context: context, textFields: [ + DialogTextField( + minLines: 1, + maxLines: 1, + obscureText: true, + ) + ]); + if (input?.isEmpty ?? true) return; + final valid = + await SimpleDialogs(context).tryRequestWithLoadingDialog( + bootstrap.newSsssKey.unlock(keyOrPassphrase: input.single), + ); + if (valid != false) bootstrap.openExistingSsss(); + })); + break; + case BootstrapState.askWipeCrossSigning: + body = Text('Wipe cross-signing?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.wipeCrossSigning(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.wipeCrossSigning(false), + )); + break; + case BootstrapState.askSetupCrossSigning: + body = Text('Set up cross-signing?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.askSetupCrossSigning( + setupMasterKey: true, + setupSelfSigningKey: true, + setupUserSigningKey: true, + ), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.askSetupCrossSigning(), + )); + break; + case BootstrapState.askWipeOnlineKeyBackup: + body = Text('Wipe chat backup?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.wipeOnlineKeyBackup(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.wipeOnlineKeyBackup(false), + )); + break; + case BootstrapState.askSetupOnlineKeyBackup: + body = Text('Set up chat backup?'); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).yes), + onPressed: () => bootstrap.askSetupOnlineKeyBackup(true), + )); + buttons.add(AdaptiveFlatButton( + textColor: Theme.of(context).textTheme.bodyText1.color, + child: Text(L10n.of(context).no), + onPressed: () => bootstrap.askSetupOnlineKeyBackup(false), + )); + break; + case BootstrapState.error: + body = ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.error_outline, color: Colors.red), + title: Text(L10n.of(context).oopsSomethingWentWrong), + ); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).close), + onPressed: () => Navigator.of(context).pop(false), + )); + break; + case BootstrapState.done: + body = ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.check_circle, color: Colors.green), + title: Text('Chat backup has been initialized!'), + ); + buttons.add(AdaptiveFlatButton( + child: Text(L10n.of(context).close), + onPressed: () => Navigator.of(context).pop(false), + )); + break; + } + + final title = Text('Chat backup'); + if (PlatformInfos.isCupertinoStyle) { + return CupertinoAlertDialog( + title: title, + content: body, + actions: buttons, + ); + } + return AlertDialog( + title: title, + content: body, + actions: buttons, + ); + } +} + +class _AskUnlockOldSsss extends StatefulWidget { + final String keyId; + final OpenSSSS ssssKey; + _AskUnlockOldSsss(this.keyId, this.ssssKey); + + @override + _AskUnlockOldSsssState createState() => _AskUnlockOldSsssState(); +} + +class _AskUnlockOldSsssState extends State<_AskUnlockOldSsss> { + bool valid = false; + TextEditingController textEditingController = TextEditingController(); + String input; + + void checkInput(BuildContext context) async { + if (input == null) { + return; + } + + valid = await SimpleDialogs(context).tryRequestWithLoadingDialog( + widget.ssssKey.unlock(keyOrPassphrase: input), + ); + setState(() => null); + } + + @override + Widget build(BuildContext build) { + if (valid) { + return Row( + children: [ + Text(widget.keyId), + Text('unlocked'), + ], + mainAxisSize: MainAxisSize.min, + ); + } + return Row( + children: [ + Text(widget.keyId), + Flexible( + child: TextField( + controller: textEditingController, + autofocus: false, + autocorrect: false, + onSubmitted: (s) { + input = s; + checkInput(context); + }, + minLines: 1, + maxLines: 1, + obscureText: true, + decoration: InputDecoration( + hintText: L10n.of(context).passphraseOrKey, + prefixStyle: TextStyle(color: Theme.of(context).primaryColor), + suffixStyle: TextStyle(color: Theme.of(context).primaryColor), + border: OutlineInputBorder(), + ), + ), + ), + RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).submit), + onPressed: () { + input = textEditingController.text; + checkInput(context); + }, + ), + ], + mainAxisSize: MainAxisSize.min, + ); + } +} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 16b8687d..baf9dca2 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -155,9 +155,43 @@ class MatrixState extends State { StreamSubscription onKeyVerificationRequestSub; StreamSubscription onJitsiCallSub; StreamSubscription onNotification; + StreamSubscription onUiaRequest; StreamSubscription onFocusSub; StreamSubscription onBlurSub; + void _onUiaRequest(UiaRequest uiaRequest) async { + uiaRequest.onUpdate = () => _onUiaRequest(uiaRequest); + if (uiaRequest.loading || uiaRequest.done || uiaRequest.fail) return; + final stage = uiaRequest.nextStages.first; + switch (stage) { + case 'm.login.password': + final input = await showTextInputDialog(context: context, textFields: [ + DialogTextField( + minLines: 1, + maxLines: 1, + obscureText: true, + ) + ]); + if (input?.isEmpty ?? true) return; + return uiaRequest.completeStage( + 'm.login.password', + { + 'type': 'm.login.password', + 'identifier': { + 'type': 'm.id.user', + 'user': client.userID, + }, + 'user': client.userID, + 'password': input.single, + 'session': uiaRequest.session, + }, + ); + default: + debugPrint('Warning! Cannot handle the stage "$stage"'); + return; + } + } + void onJitsiCall(EventUpdate eventUpdate) { final event = Event.fromJson( eventUpdate.content, client.getRoomById(eventUpdate.roomID)); @@ -373,6 +407,7 @@ class MatrixState extends State { onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true); onBlurSub = html.window.onBlur.listen((_) => webHasFocus = false); } + onUiaRequest ??= client.onUiaRequest.stream.listen(_onUiaRequest); if (kIsWeb || Platform.isLinux) { client.onSync.stream.first.then((s) { html.Notification.requestPermission(); diff --git a/lib/views/login.dart b/lib/views/login.dart index a58266a0..ccaa5aae 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -131,6 +131,8 @@ class _LoginState extends State { DialogTextField( hintText: '******', obscureText: true, + minLines: 1, + maxLines: 1, ), ], ); diff --git a/lib/views/settings.dart b/lib/views/settings.dart index 95755823..15530a42 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -1,4 +1,5 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/components/dialogs/bootstrap_dialog.dart'; import 'package:fluffychat/views/settings_3pid.dart'; import 'package:fluffychat/views/settings_notifications.dart'; import 'package:fluffychat/views/settings_style.dart'; @@ -11,7 +12,6 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/sentry_controller.dart'; import 'package:fluffychat/views/settings_devices.dart'; import 'package:fluffychat/views/settings_ignore_list.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; @@ -73,10 +73,14 @@ class _SettingsState extends State { DialogTextField( hintText: L10n.of(context).pleaseEnterYourPassword, obscureText: true, + minLines: 1, + maxLines: 1, ), DialogTextField( hintText: L10n.of(context).chooseAStrongPassword, obscureText: true, + minLines: 1, + maxLines: 1, ), ], ); @@ -108,7 +112,14 @@ class _SettingsState extends State { final input = await showTextInputDialog( context: context, title: L10n.of(context).pleaseEnterYourPassword, - textFields: [DialogTextField(obscureText: true, hintText: '******')], + textFields: [ + DialogTextField( + obscureText: true, + hintText: '******', + minLines: 1, + maxLines: 1, + ) + ], ); if (input == null) return; await SimpleDialogs(context).tryRequestWithLoadingDialog( @@ -208,7 +219,11 @@ class _SettingsState extends State { title: L10n.of(context).askSSSSCache, textFields: [ DialogTextField( - hintText: L10n.of(context).passphraseOrKey, obscureText: true) + hintText: L10n.of(context).passphraseOrKey, + obscureText: true, + minLines: 1, + maxLines: 1, + ) ], ); if (input != null) { @@ -221,16 +236,7 @@ class _SettingsState extends State { await handle.unlock(recoveryKey: input.single); valid = true; } catch (e, s) { - debugPrint('Couldn\'t use recovery key: ' + e.toString()); - debugPrint(s.toString()); - try { - await handle.unlock(passphrase: input.single); - valid = true; - } catch (e, s) { - debugPrint('Couldn\'t use recovery passphrase: ' + e.toString()); - debugPrint(s.toString()); - valid = false; - } + SentryController.captureException(e, s); } return valid; })); @@ -499,11 +505,7 @@ class _SettingsState extends State { : null, onTap: () async { if (!client.encryption.crossSigning.enabled) { - await showOkAlertDialog( - context: context, - message: L10n.of(context).noCrossSignBootstrap, - ); - return; + return BootstrapDialog().show(context); } if (client.isUnknownSession) { final input = await showTextInputDialog( @@ -511,8 +513,11 @@ class _SettingsState extends State { title: L10n.of(context).askSSSSVerify, textFields: [ DialogTextField( - hintText: L10n.of(context).passphraseOrKey, - obscureText: true) + hintText: L10n.of(context).passphraseOrKey, + obscureText: true, + minLines: 1, + maxLines: 1, + ) ], ); if (input != null) { @@ -575,11 +580,7 @@ class _SettingsState extends State { : null, onTap: () async { if (!client.encryption.keyManager.enabled) { - await showOkAlertDialog( - context: context, - message: L10n.of(context).noMegolmBootstrap, - ); - return; + return BootstrapDialog().show(context); } if (!(await client.encryption.keyManager.isCached())) { await requestSSSSCache(context); diff --git a/lib/views/settings_3pid.dart b/lib/views/settings_3pid.dart index d5b5fe82..f6c4f624 100644 --- a/lib/views/settings_3pid.dart +++ b/lib/views/settings_3pid.dart @@ -62,6 +62,8 @@ class _Settings3PidState extends State { DialogTextField( hintText: '******', obscureText: true, + minLines: 1, + maxLines: 1, ), ], ); diff --git a/lib/views/settings_devices.dart b/lib/views/settings_devices.dart index 510d3a39..e0f782b0 100644 --- a/lib/views/settings_devices.dart +++ b/lib/views/settings_devices.dart @@ -53,6 +53,8 @@ class DevicesSettingsState extends State { DialogTextField( hintText: '******', obscureText: true, + minLines: 1, + maxLines: 1, ) ], ); diff --git a/pubspec.lock b/pubspec.lock index 90dd3b15..16146f52 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,8 +201,8 @@ packages: dependency: "direct main" description: path: "." - ref: b563aec7bbac32ec08cdea89d04b4a3fbdb6ca57 - resolved-ref: b563aec7bbac32ec08cdea89d04b4a3fbdb6ca57 + ref: "4a9bd36c74d9db001e6d6206403d7b8fe61a4d1f" + resolved-ref: "4a9bd36c74d9db001e6d6206403d7b8fe61a4d1f" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3cab230a..1b1f4e7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: b563aec7bbac32ec08cdea89d04b4a3fbdb6ca57 + ref: 4a9bd36c74d9db001e6d6206403d7b8fe61a4d1f localstorage: ^3.0.3+6 file_picker_cross: 4.2.2