feat: Implement experimental bootstrapping

This commit is contained in:
Christian Pauly 2020-12-10 15:06:02 +01:00
parent 775a33bb94
commit f6945f7476
8 changed files with 364 additions and 28 deletions

View File

@ -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<bool> 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<BootstrapDialog> {
Bootstrap bootstrap;
@override
Widget build(BuildContext context) {
bootstrap ??= Matrix.of(context)
.client
.encryption
.bootstrap(onUpdate: () => setState(() => null));
final buttons = <AdaptiveFlatButton>[];
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 = <Widget>[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<bool>(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<bool>(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: <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: 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,
);
}
}

View File

@ -155,9 +155,43 @@ class MatrixState extends State<Matrix> {
StreamSubscription onKeyVerificationRequestSub;
StreamSubscription onJitsiCallSub;
StreamSubscription onNotification;
StreamSubscription<UiaRequest> onUiaRequest;
StreamSubscription<html.Event> onFocusSub;
StreamSubscription<html.Event> 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<Matrix> {
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();

View File

@ -131,6 +131,8 @@ class _LoginState extends State<Login> {
DialogTextField(
hintText: '******',
obscureText: true,
minLines: 1,
maxLines: 1,
),
],
);

View File

@ -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<Settings> {
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<Settings> {
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<Settings> {
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<Settings> {
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<Settings> {
: 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<Settings> {
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<Settings> {
: 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);

View File

@ -62,6 +62,8 @@ class _Settings3PidState extends State<Settings3Pid> {
DialogTextField(
hintText: '******',
obscureText: true,
minLines: 1,
maxLines: 1,
),
],
);

View File

@ -53,6 +53,8 @@ class DevicesSettingsState extends State<DevicesSettings> {
DialogTextField(
hintText: '******',
obscureText: true,
minLines: 1,
maxLines: 1,
)
],
);

View File

@ -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"

View File

@ -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