diff --git a/lib/components/dialogs/uia_dialog.dart b/lib/components/dialogs/uia_dialog.dart new file mode 100644 index 00000000..ccfd0667 --- /dev/null +++ b/lib/components/dialogs/uia_dialog.dart @@ -0,0 +1,121 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../matrix.dart'; + +class UiaDialog extends StatefulWidget { + final UiaRequest uia; + + UiaDialog({this.uia}); + + @override + _UiaDialogState createState() => _UiaDialogState(); +} + +class _UiaDialogState extends State { + bool loading = true; + + @override + void initState() { + loading = widget.uia.nextStages.isEmpty; // initial test if the first reply returned + widget.uia.onUpdate = () { + loading = false; + setState(() => null); + }; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (loading) { + return AlertDialog( + title: Text(L10n.of(context).loadingPleaseWait), + content: LinearProgressIndicator(), + ); + } + if (widget.uia.fail) { + return AlertDialog( + title: Text('UIA fail'), + actions: [ + FlatButton( + child: Text('Close'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } + if (widget.uia.done) { + return AlertDialog( + title: Text('UIA done'), + actions: [ + FlatButton( + child: Text('Close'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } + if (widget.uia.nextStages.isEmpty) { + return AlertDialog( + title: Text('UIA error'), + actions: [ + FlatButton( + child: Text('Close'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } + // TODO: pick next stage + final nextStage = widget.uia.nextStages.first; + switch (nextStage) { + case 'm.login.password': + final controller = TextEditingController(); + String input; + final submit = () { + final client = Matrix.of(context).client; + final auth = { + 'password': input, + 'user': client.userID, + 'identifier': { + 'type': 'm.id.user', + 'user': client.userID, + }, + }; + widget.uia.completeStage('m.login.password', auth); + setState(() => loading = true); + }; + return AlertDialog( + title: Text('Ender password'), + content: TextField( + controller: controller, + autofocus: true, + autocorrect: false, + onSubmitted: (s) { + input = s; + submit(); + }, + minLines: 1, + maxLines: 1, + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + ), + actions: [ + FlatButton( + child: Text('Submit'), + onPressed: () { + input = controller.text; + submit(); + }, + ), + ], + ); + default: + return AlertDialog( + title: Text('Unknown UIA stage ' + nextStage), + ); + } + } +} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 4f359959..584bbe60 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -24,6 +24,7 @@ import '../utils/famedlysdk_store.dart'; import '../views/key_verification.dart'; import '../utils/platform_infos.dart'; import 'avatar.dart'; +import 'dialogs/uia_dialog.dart'; class Matrix extends StatefulWidget { static const String callNamespace = 'chat.fluffy.jitsi_call'; @@ -118,6 +119,7 @@ class MatrixState extends State { StreamSubscription onKeyVerificationRequestSub; StreamSubscription onJitsiCallSub; StreamSubscription onNotification; + StreamSubscription onUiaRequestSub; StreamSubscription onFocusSub; StreamSubscription onBlurSub; @@ -297,6 +299,12 @@ class MatrixState extends State { await request.rejectVerification(); } }); + onUiaRequestSub ??= client.onUiaRequest.stream.listen((UiaRequest uia) { + showDialog( + context: context, + builder: (c) => UiaDialog(uia: uia), + ); + }); _initWithStore(); } else { client = widget.client; @@ -342,6 +350,7 @@ class MatrixState extends State { onKeyVerificationRequestSub?.cancel(); onJitsiCallSub?.cancel(); onNotification?.cancel(); + onUiaRequestSub?.cancel(); onFocusSub?.cancel(); onBlurSub?.cancel(); super.dispose(); diff --git a/lib/views/bootstrap.dart b/lib/views/bootstrap.dart new file mode 100644 index 00000000..c6c1ef0e --- /dev/null +++ b/lib/views/bootstrap.dart @@ -0,0 +1,344 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../components/adaptive_page_layout.dart'; +import '../components/avatar.dart'; +import '../components/dialogs/simple_dialogs.dart'; +import '../components/matrix.dart'; +import 'chat_list.dart'; + +class BootstrapView extends StatelessWidget { + BootstrapView(); + + @override + Widget build(BuildContext context) { + + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: BootstrapPage(Matrix.of(context).client), + ); + } +} + +class BootstrapPage extends StatefulWidget { + final Client client; + BootstrapPage(this.client); + + @override + _BootstrapPageState createState() => _BootstrapPageState(); +} + +class _BootstrapPageState extends State { + Bootstrap bootstrap; + + @override + void initState() { + bootstrap = widget.client.encryption.bootstrap(onUpdate: () => setState(() => null)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Widget body; + final buttons = []; + switch (bootstrap.state) { + case BootstrapState.loading: + body = CircularProgressIndicator(); + break; + case BootstrapState.askWipeSsss: + body = Text('Wipe SSSS?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.wipeSsss(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.wipeSsss(false), + )); + break; + case BootstrapState.askUseExistingSsss: + body = Text('Use existing SSSS?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.useExistingSsss(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.useExistingSsss(false), + )); + break; + case BootstrapState.askBadSsss: + body = Text('SSSS bad - continue nevertheless? DATALOSS!!!'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.ignoreBadSecrets(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('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(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('Done'), + onPressed: () => bootstrap.unlockedSsss(), + )); + break; + case BootstrapState.askNewSsss: + String passphrase; + body = Column( + children: [ + Text('New SSSS passphrase'), + Flexible( + child: TextField( + autofocus: false, + autocorrect: false, + onChanged: (s) { + passphrase = s; + }, + minLines: 1, + maxLines: 1, + obscureText: true, + decoration: InputDecoration( + hintText: 'New passphrase', + prefixStyle: TextStyle(color: Theme.of(context).primaryColor), + suffixStyle: TextStyle(color: Theme.of(context).primaryColor), + border: OutlineInputBorder(), + ), + ), + ), + ], + mainAxisSize: MainAxisSize.min, + ); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('Done'), + onPressed: () => bootstrap.newSsss(passphrase?.isNotEmpty ?? false ? passphrase : null), + )); + break; + case BootstrapState.openExistingSsss: + body = Column( + children: [ + Text('Existing SSSS passphrase'), + Flexible(child: _AskUnlockOldSsss('existing', bootstrap.newSsssKey)), + ], + mainAxisSize: MainAxisSize.min, + ); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('Done'), + onPressed: () => bootstrap.openExistingSsss(), + )); + break; + case BootstrapState.askWipeCrossSigning: + body = Text('Wipe cross-signing?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.wipeCrossSigning(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.wipeCrossSigning(false), + )); + break; + case BootstrapState.askSetupCrossSigning: + body = Text('Set up cross-signing?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.askSetupCrossSigning( + setupMasterKey: true, + setupSelfSigningKey: true, + setupUserSigningKey: true, + ), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.askSetupCrossSigning(), + )); + break; + case BootstrapState.askWipeOnlineKeyBackup: + body = Text('Wipe online key backup?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.wipeOnlineKeyBackup(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.wipeOnlineKeyBackup(false), + )); + break; + case BootstrapState.askSetupOnlineKeyBackup: + body = Text('Set up online key backup?'); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text('Yes'), + onPressed: () => bootstrap.askSetupOnlineKeyBackup(true), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text('No'), + onPressed: () => bootstrap.askSetupOnlineKeyBackup(false), + )); + break; + case BootstrapState.error: + body = Icon(Icons.cancel, color: Colors.red, size: 200.0); + break; + case BootstrapState.done: + body = Icon(Icons.check_circle, color: Colors.green, size: 200.0); + break; + } + body ??= Text('ERROR: Unknown state ' + bootstrap.state.toString()); + return Scaffold( + appBar: AppBar( + title: Text('Bootstrapping'), + ), + extendBody: true, + extendBodyBehindAppBar: true, + body: Center( + child: body, + ), + persistentFooterButtons: buttons.isEmpty ? null : 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; + } + SimpleDialogs(context).showLoadingDialog(context); + valid = false; + try { + await widget.ssssKey.unlock(keyOrPassphrase: input); + valid = true; + } catch (_) { + valid = false; + } + await Navigator.of(context)?.pop(); + 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/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index 8d6fbcfe..0c292f67 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -27,9 +27,9 @@ class HomeserverPicker extends StatelessWidget { await SentryController.toggleSentryAction(context); } - if (!homeserver.startsWith('https://')) { - homeserver = 'https://$homeserver'; - } +// if (!homeserver.startsWith('https://')) { +// homeserver = 'https://$homeserver'; +// } final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( checkHomeserver(homeserver, Matrix.of(context).client)); diff --git a/lib/views/key_verification.dart b/lib/views/key_verification.dart index 59585cd5..f8add743 100644 --- a/lib/views/key_verification.dart +++ b/lib/views/key_verification.dart @@ -78,19 +78,12 @@ class _KeyVerificationPageState extends State { return; } SimpleDialogs(context).showLoadingDialog(context); - // make sure the loading spinner shows before we test the keys - await Future.delayed(Duration(milliseconds: 100)); var valid = false; try { - await widget.request.openSSSS(recoveryKey: input); + await widget.request.openSSSS(keyOrPassphrase: input); valid = true; } catch (_) { - try { - await widget.request.openSSSS(passphrase: input); - valid = true; - } catch (_) { - valid = false; - } + valid = false; } await Navigator.of(context)?.pop(); if (!valid) { diff --git a/lib/views/settings.dart b/lib/views/settings.dart index bd4d4a54..79576948 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -24,6 +24,7 @@ import '../utils/app_route.dart'; import 'app_info.dart'; import 'chat_list.dart'; import 'settings_emotes.dart'; +import 'bootstrap.dart'; class SettingsView extends StatelessWidget { @override @@ -198,23 +199,12 @@ class _SettingsState extends State { ); if (str != null) { SimpleDialogs(context).showLoadingDialog(context); - // make sure the loading spinner shows before we test the keys - await Future.delayed(Duration(milliseconds: 100)); var valid = false; try { - handle.unlock(recoveryKey: str); + await handle.unlock(keyOrPassphrase: str); valid = true; } catch (e, s) { - debugPrint('Couldn\'t use recovery key: ' + e.toString()); - debugPrint(s.toString()); - try { - handle.unlock(passphrase: str); - valid = true; - } catch (e, s) { - debugPrint('Couldn\'t use recovery passphrase: ' + e.toString()); - debugPrint(s.toString()); - valid = false; - } + valid = false; } await Navigator.of(context)?.pop(); if (valid) { @@ -466,10 +456,16 @@ class _SettingsState extends State { : L10n.of(context).keysMissing))) : null, onTap: () async { - if (!client.encryption.crossSigning.enabled) { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).noCrossSignBootstrap, + if (true || !client.encryption.crossSigning.enabled) { + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + BootstrapView(), + ), ); + /*await SimpleDialogs(context).inform( + contentText: L10n.of(context).noCrossSignBootstrap, + );*/ return; } if (client.isUnknownSession) {