diff --git a/lib/components/dialogs/bootstrap_dialog.dart b/lib/components/dialogs/bootstrap_dialog.dart index b70c08e3..9ff7c9ea 100644 --- a/lib/components/dialogs/bootstrap_dialog.dart +++ b/lib/components/dialogs/bootstrap_dialog.dart @@ -3,7 +3,8 @@ import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/encryption/utils/bootstrap.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/dialogs/adaptive_flat_button.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:fluffychat/utils/sentry_controller.dart'; +import 'package:flutter/services.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -14,7 +15,6 @@ class BootstrapDialog extends StatefulWidget { Key key, @required this.l10n, @required this.client, - this.easyMode = false, }) : super(key: key); Future show(BuildContext context) => PlatformInfos.isCupertinoStyle @@ -23,195 +23,224 @@ class BootstrapDialog extends StatefulWidget { final L10n l10n; final Client client; - final bool easyMode; @override _BootstrapDialogState createState() => _BootstrapDialogState(); } class _BootstrapDialogState extends State { + final TextEditingController _recoveryKeyTextEditingController = + TextEditingController(); + Bootstrap bootstrap; + String _recoveryKeyInputError; + + bool _recoveryKeyInputLoading = false; + + String titleText; + + bool _recoveryKeyStored = false; + + bool _wipe = false; + + void _createBootstrap(bool wipe) { + setState(() { + _wipe = wipe; + titleText = null; + _recoveryKeyStored = false; + bootstrap = widget.client.encryption + .bootstrap(onUpdate: () => setState(() => null)); + }); + } + @override Widget build(BuildContext context) { - bootstrap ??= widget.client.encryption - .bootstrap(onUpdate: () => setState(() => null)); - final buttons = []; - Widget body; - var titleText = widget.l10n.cachedKeys; + Widget body = LinearProgressIndicator(); + titleText = widget.l10n.loadingPleaseWait; - switch (bootstrap.state) { - case BootstrapState.loading: - body = LinearProgressIndicator(); - titleText = widget.l10n.loadingPleaseWait; - break; - case BootstrapState.askWipeSsss: - body = Text('Wipe chat backup?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.wipeSsss(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.wipeSsss(false), - )); - break; - case BootstrapState.askUseExistingSsss: - body = Text('Use existing chat backup?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.useExistingSsss(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.useExistingSsss(false), - )); - break; - case BootstrapState.askBadSsss: - body = Text('SSSS bad - continue nevertheless? DATALOSS!!!'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.ignoreBadSecrets(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.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, widget.l10n))); - } - body = Column( - children: widgets, - mainAxisSize: MainAxisSize.min, - ); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.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 showFutureLoadingDialog( - context: context, - future: () => - bootstrap.newSsssKey.unlock(keyOrPassphrase: input.single), - ); - if (valid.error == null) await bootstrap.openExistingSsss(); - })); - break; - case BootstrapState.askWipeCrossSigning: - body = Text('Wipe cross-signing?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.wipeCrossSigning(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.wipeCrossSigning(false), - )); - break; - case BootstrapState.askSetupCrossSigning: - body = Text('Set up cross-signing?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.askSetupCrossSigning( - setupMasterKey: true, - setupSelfSigningKey: true, - setupUserSigningKey: true, + if (bootstrap == null) { + titleText = 'Chat backup'; + body = Text( + 'To make sure that only you have access to your encrypted messages, we have generated a security key for you.'); + buttons.add(AdaptiveFlatButton( + child: Text(widget.l10n.next), + onPressed: () => _createBootstrap(false), + )); + } else if (bootstrap.newSsssKey?.recoveryKey != null && + _recoveryKeyStored == false) { + final key = bootstrap.newSsssKey.recoveryKey; + titleText = 'Security key'; + body = Container( + alignment: Alignment.center, + width: 200, + height: 128, + child: Text( + key, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + wordSpacing: 38, ), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.askSetupCrossSigning(), - )); - break; - case BootstrapState.askWipeOnlineKeyBackup: - body = Text('Wipe chat backup?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.wipeOnlineKeyBackup(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.wipeOnlineKeyBackup(false), - )); - break; - case BootstrapState.askSetupOnlineKeyBackup: - body = Text('Set up chat backup?'); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.yes), - onPressed: () => bootstrap.askSetupOnlineKeyBackup(true), - )); - buttons.add(AdaptiveFlatButton( - textColor: Theme.of(context).textTheme.bodyText1.color, - child: Text(widget.l10n.no), - onPressed: () => bootstrap.askSetupOnlineKeyBackup(false), - )); - break; - case BootstrapState.error: - body = ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon(Icons.error_outline, color: Colors.red), - title: Text(widget.l10n.oopsSomethingWentWrong), - ); - buttons.add(AdaptiveFlatButton( - child: Text(widget.l10n.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(widget.l10n.close), - onPressed: () => Navigator.of(context).pop(false), - )); - break; + ), + ); + buttons.add(AdaptiveFlatButton( + child: Text('Copy to clipboard'), + onPressed: () => Clipboard.setData(ClipboardData(text: key)), + )); + buttons.add(AdaptiveFlatButton( + child: Text(widget.l10n.next), + onPressed: () => setState(() => _recoveryKeyStored = true), + )); + } else { + switch (bootstrap.state) { + case BootstrapState.loading: + break; + case BootstrapState.askWipeSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.wipeSsss(_wipe), + ); + break; + case BootstrapState.askUseExistingSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.useExistingSsss(!_wipe), + ); + break; + case BootstrapState.askUnlockSsss: + throw Exception('This state is not supposed to be implemented'); + case BootstrapState.askNewSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.newSsss(), + ); + break; + case BootstrapState.openExistingSsss: + _recoveryKeyStored = true; + titleText = + _recoveryKeyInputError ?? 'Please enter your security key!'; + body = PlatformInfos.isCupertinoStyle + ? CupertinoTextField( + minLines: 2, + maxLines: 2, + autofocus: true, + autocorrect: false, + autofillHints: _recoveryKeyInputLoading + ? null + : [AutofillHints.password], + controller: _recoveryKeyTextEditingController, + ) + : TextField( + minLines: 2, + maxLines: 2, + autofocus: true, + autocorrect: false, + autofillHints: _recoveryKeyInputLoading + ? null + : [AutofillHints.password], + controller: _recoveryKeyTextEditingController, + ); + buttons.add(AdaptiveFlatButton( + textColor: Colors.red, + child: Text('Lost security key'), + onPressed: () async { + if (OkCancelResult.ok == + await showOkCancelAlertDialog( + context: context, + title: 'Lost security key', + message: + 'Wipe your chat backup to create a new security key?', + isDestructiveAction: true, + )) { + _createBootstrap(true); + } + }, + )); + buttons.add(AdaptiveFlatButton( + child: Text(widget.l10n.next), + onPressed: () async { + setState(() { + _recoveryKeyInputError = null; + _recoveryKeyInputLoading = true; + }); + try { + final input = _recoveryKeyTextEditingController.text.trim(); + await bootstrap.newSsssKey.unlock( + keyOrPassphrase: input, + ); + await bootstrap.openExistingSsss(); + if (widget.client.encryption.crossSigning.enabled) { + Logs().v( + 'Cross signing is already enabled. Try to self-sign'); + try { + await widget.client.encryption.crossSigning + .selfSign(recoveryKey: input); + } catch (e, s) { + // ignore: unawaited_futures + SentryController.captureException( + 'Unable to self sign with recovery key after successfully open existing SSSS: ${e.toString()}', + s); + } + } + } catch (e, s) { + Logs().w('Unable to unlock SSSS', e, s); + setState(() => _recoveryKeyInputError = + L10n.of(context).oopsSomethingWentWrong); + } finally { + setState(() => _recoveryKeyInputLoading = false); + } + })); + break; + case BootstrapState.askWipeCrossSigning: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.wipeCrossSigning(_wipe), + ); + break; + case BootstrapState.askSetupCrossSigning: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.askSetupCrossSigning( + setupMasterKey: true, + setupSelfSigningKey: true, + setupUserSigningKey: true, + ), + ); + break; + case BootstrapState.askWipeOnlineKeyBackup: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.wipeOnlineKeyBackup(_wipe), + ); + + break; + case BootstrapState.askSetupOnlineKeyBackup: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap.askSetupOnlineKeyBackup(true), + ); + break; + case BootstrapState.askBadSsss: + case BootstrapState.error: + titleText = widget.l10n.oopsSomethingWentWrong; + body = ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.error_outline, color: Colors.red), + title: Text(widget.l10n.oopsSomethingWentWrong), + ); + buttons.add(AdaptiveFlatButton( + child: Text(widget.l10n.close), + onPressed: () => Navigator.of(context).pop(false), + )); + break; + case BootstrapState.done: + titleText = 'Process completed'; + 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(widget.l10n.close), + onPressed: () => Navigator.of(context).pop(false), + )); + break; + } } final title = Text(titleText); @@ -229,82 +258,3 @@ class _BootstrapDialogState extends State { ); } } - -class _AskUnlockOldSsss extends StatefulWidget { - final String keyId; - final OpenSSSS ssssKey; - final L10n l10n; - _AskUnlockOldSsss(this.keyId, this.ssssKey, this.l10n); - - @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 showFutureLoadingDialog( - context: context, - future: () => widget.ssssKey.unlock(keyOrPassphrase: input), - )) - .error == - null; - 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: widget.l10n.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(widget.l10n.submit), - onPressed: () { - input = textEditingController.text; - checkInput(context); - }, - ), - ], - mainAxisSize: MainAxisSize.min, - ); - } -}