mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2025-04-17 06:37:51 +02:00
feat: add bootstrapping
This commit is contained in:
parent
d89512e495
commit
b6a48b92a0
121
lib/components/dialogs/uia_dialog.dart
Normal file
121
lib/components/dialogs/uia_dialog.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
344
lib/views/bootstrap.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user