feat: Simplified bootstrap

onboarding
Christian Pauly 5 years ago
parent 72a2e5ee36
commit d16b7a208d

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

Loading…
Cancel
Save