feat: add bootstrapping

This commit is contained in:
Sorunome 2020-11-07 11:17:56 +01:00
parent d89512e495
commit b6a48b92a0
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
6 changed files with 491 additions and 28 deletions

View File

@ -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<UiaDialog> {
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: <Widget>[
FlatButton(
child: Text('Close'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
}
if (widget.uia.done) {
return AlertDialog(
title: Text('UIA done'),
actions: <Widget>[
FlatButton(
child: Text('Close'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
}
if (widget.uia.nextStages.isEmpty) {
return AlertDialog(
title: Text('UIA error'),
actions: <Widget>[
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 = <String, dynamic>{
'password': input,
'user': client.userID,
'identifier': <String, dynamic>{
'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: <Widget>[
FlatButton(
child: Text('Submit'),
onPressed: () {
input = controller.text;
submit();
},
),
],
);
default:
return AlertDialog(
title: Text('Unknown UIA stage ' + nextStage),
);
}
}
}

View File

@ -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<Matrix> {
StreamSubscription onKeyVerificationRequestSub;
StreamSubscription onJitsiCallSub;
StreamSubscription onNotification;
StreamSubscription onUiaRequestSub;
StreamSubscription<html.Event> onFocusSub;
StreamSubscription<html.Event> onBlurSub;
@ -297,6 +299,12 @@ class MatrixState extends State<Matrix> {
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<Matrix> {
onKeyVerificationRequestSub?.cancel();
onJitsiCallSub?.cancel();
onNotification?.cancel();
onUiaRequestSub?.cancel();
onFocusSub?.cancel();
onBlurSub?.cancel();
super.dispose();

344
lib/views/bootstrap.dart Normal file
View File

@ -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<BootstrapPage> {
Bootstrap bootstrap;
@override
void initState() {
bootstrap = widget.client.encryption.bootstrap(onUpdate: () => setState(() => null));
super.initState();
}
@override
Widget build(BuildContext context) {
Widget body;
final buttons = <Widget>[];
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 = <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(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: <Widget>[
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: <Widget>[
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: <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

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

View File

@ -78,19 +78,12 @@ class _KeyVerificationPageState extends State<KeyVerificationPage> {
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) {

View File

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