fluffychat/lib/pages/bootstrap/bootstrap_dialog.dart

460 lines
18 KiB
Dart
Raw Normal View History

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
2021-10-26 18:50:34 +02:00
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2021-05-15 12:06:13 +02:00
import 'package:future_loading_dialog/future_loading_dialog.dart';
2021-10-26 18:50:34 +02:00
import 'package:matrix/encryption.dart';
import 'package:matrix/encryption/utils/bootstrap.dart';
import 'package:matrix/matrix.dart';
2022-05-06 08:58:59 +02:00
import 'package:share_plus/share_plus.dart';
2021-10-26 18:50:34 +02:00
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/adaptive_flat_button.dart';
2021-11-09 21:32:16 +01:00
import '../key_verification/key_verification_dialog.dart';
2021-02-13 14:26:03 +01:00
class BootstrapDialog extends StatefulWidget {
2021-02-13 14:26:03 +01:00
final bool wipe;
2021-03-06 10:03:01 +01:00
final Client client;
2021-01-23 11:57:47 +01:00
const BootstrapDialog({
2022-01-29 12:35:03 +01:00
Key? key,
2021-02-13 14:26:03 +01:00
this.wipe = false,
2022-01-29 12:35:03 +01:00
required this.client,
2021-01-23 11:57:47 +01:00
}) : super(key: key);
2021-01-19 15:46:43 +01:00
2022-01-29 12:35:03 +01:00
Future<bool?> show(BuildContext context) => PlatformInfos.isCupertinoStyle
2021-02-24 12:17:23 +01:00
? showCupertinoDialog(
context: context,
builder: (context) => this,
2021-04-09 15:53:26 +02:00
barrierDismissible: true,
2021-05-23 15:02:36 +02:00
useRootNavigator: false,
2021-02-24 12:17:23 +01:00
)
: showDialog(
context: context,
builder: (context) => this,
2021-04-09 15:53:26 +02:00
barrierDismissible: true,
2021-05-23 15:02:36 +02:00
useRootNavigator: false,
2021-02-24 12:17:23 +01:00
);
2021-01-19 15:46:43 +01:00
@override
2022-08-14 16:59:21 +02:00
BootstrapDialogState createState() => BootstrapDialogState();
}
2022-08-14 16:59:21 +02:00
class BootstrapDialogState extends State<BootstrapDialog> {
2021-02-06 12:55:27 +01:00
final TextEditingController _recoveryKeyTextEditingController =
TextEditingController();
2022-01-29 12:35:03 +01:00
late Bootstrap bootstrap;
2022-01-29 12:35:03 +01:00
String? _recoveryKeyInputError;
2021-02-06 12:55:27 +01:00
bool _recoveryKeyInputLoading = false;
2022-01-29 12:35:03 +01:00
String? titleText;
2021-02-06 12:55:27 +01:00
bool _recoveryKeyStored = false;
bool _recoveryKeyCopied = false;
2021-02-06 12:55:27 +01:00
bool? _storeInSecureStorage = false;
2022-01-29 12:35:03 +01:00
bool? _wipe;
2021-02-06 12:55:27 +01:00
String get _secureStorageKey =>
'ssss_recovery_key_${bootstrap.client.userID}';
bool get _supportsSecureStorage =>
PlatformInfos.isMobile || PlatformInfos.isDesktop;
String _getSecureStorageLocalizedName() {
if (PlatformInfos.isAndroid) {
return L10n.of(context)!.storeInAndroidKeystore;
}
if (PlatformInfos.isIOS || PlatformInfos.isMacOS) {
return L10n.of(context)!.storeInAppleKeyChain;
}
return L10n.of(context)!.storeSecurlyOnThisDevice;
}
2021-11-19 09:12:47 +01:00
@override
void initState() {
_createBootstrap(widget.wipe);
super.initState();
}
2022-08-05 20:37:36 +02:00
void _createBootstrap(bool wipe) async {
2021-11-19 09:12:47 +01:00
_wipe = wipe;
titleText = null;
_recoveryKeyStored = false;
2022-01-29 12:35:03 +01:00
bootstrap =
2022-09-05 17:44:16 +02:00
widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {}));
2022-08-05 20:37:36 +02:00
final key = await const FlutterSecureStorage().read(key: _secureStorageKey);
if (key == null) return;
_recoveryKeyTextEditingController.text = key;
2021-02-06 12:55:27 +01:00
}
@override
Widget build(BuildContext context) {
2021-02-13 14:26:03 +01:00
_wipe ??= widget.wipe;
final buttons = <AdaptiveFlatButton>[];
2021-06-22 17:56:15 +02:00
Widget body = PlatformInfos.isCupertinoStyle
2021-10-14 18:09:30 +02:00
? const CupertinoActivityIndicator()
: const LinearProgressIndicator();
2022-01-29 12:35:03 +01:00
titleText = L10n.of(context)!.loadingPleaseWait;
2021-11-19 09:12:47 +01:00
if (bootstrap.newSsssKey?.recoveryKey != null &&
2021-02-06 12:55:27 +01:00
_recoveryKeyStored == false) {
2022-01-29 12:35:03 +01:00
final key = bootstrap.newSsssKey!.recoveryKey;
titleText = L10n.of(context)!.recoveryKey;
2021-10-14 16:57:01 +02:00
return Scaffold(
appBar: AppBar(
2021-10-14 16:56:08 +02:00
centerTitle: true,
leading: IconButton(
2021-10-14 18:09:30 +02:00
icon: const Icon(Icons.close),
2021-10-14 16:56:08 +02:00
onPressed: Navigator.of(context).pop,
2021-10-10 13:35:16 +02:00
),
title: Text(L10n.of(context)!.recoveryKey),
2021-10-14 16:56:08 +02:00
),
2021-10-14 16:57:01 +02:00
body: Center(
2021-10-14 16:56:08 +02:00
child: ConstrainedBox(
constraints:
2021-10-14 18:09:30 +02:00
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 1.5),
2021-10-14 16:56:08 +02:00
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
2023-02-17 09:34:23 +01:00
trailing: CircleAvatar(
backgroundColor: Colors.transparent,
child: Icon(
Icons.info_outlined,
color: Theme.of(context).colorScheme.primary,
),
),
subtitle: Text(L10n.of(context)!.chatBackupDescription),
),
const Divider(
height: 32,
thickness: 1,
),
2021-10-14 16:56:08 +02:00
TextField(
2023-02-17 09:34:23 +01:00
minLines: 2,
2021-10-14 16:56:08 +02:00
maxLines: 4,
readOnly: true,
2023-02-17 09:34:23 +01:00
style: const TextStyle(fontFamily: 'RobotoMono'),
2021-10-14 16:56:08 +02:00
controller: TextEditingController(text: key),
2023-02-17 09:34:23 +01:00
decoration: const InputDecoration(
contentPadding: EdgeInsets.all(16),
suffixIcon: Icon(Icons.key_outlined),
),
2021-10-10 13:35:16 +02:00
),
2021-10-14 16:56:08 +02:00
const SizedBox(height: 16),
if (_supportsSecureStorage)
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
value: _storeInSecureStorage,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (b) {
setState(() {
_storeInSecureStorage = b;
});
},
title: Text(_getSecureStorageLocalizedName()),
subtitle:
Text(L10n.of(context)!.storeInSecureStorageDescription),
),
const SizedBox(height: 16),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
value: _recoveryKeyCopied,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (b) {
2022-06-15 13:00:12 +02:00
final box = context.findRenderObject() as RenderBox;
Share.share(
key!,
sharePositionOrigin:
box.localToGlobal(Offset.zero) & box.size,
);
setState(() => _recoveryKeyCopied = true);
},
title: Text(L10n.of(context)!.copyToClipboard),
subtitle: Text(L10n.of(context)!.saveKeyManuallyDescription),
2021-10-14 16:56:08 +02:00
),
const SizedBox(height: 16),
ElevatedButton.icon(
2021-10-14 18:09:30 +02:00
icon: const Icon(Icons.check_outlined),
2022-01-29 12:35:03 +01:00
label: Text(L10n.of(context)!.next),
onPressed:
(_recoveryKeyCopied || _storeInSecureStorage == true)
? () {
if (_storeInSecureStorage == true) {
2022-08-05 20:37:36 +02:00
const FlutterSecureStorage().write(
key: _secureStorageKey,
value: key,
);
}
setState(() => _recoveryKeyStored = true);
}
: null,
2021-10-14 16:56:08 +02:00
),
],
),
2021-10-10 13:35:16 +02:00
),
2021-02-06 12:55:27 +01:00
),
);
} else {
switch (bootstrap.state) {
case BootstrapState.loading:
break;
case BootstrapState.askWipeSsss:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2022-01-29 12:35:03 +01:00
(_) => bootstrap.wipeSsss(_wipe!),
2021-02-06 12:55:27 +01:00
);
break;
2021-07-24 11:45:27 +02:00
case BootstrapState.askBadSsss:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2021-07-24 11:45:27 +02:00
(_) => bootstrap.ignoreBadSecrets(true),
);
break;
2021-02-06 12:55:27 +01:00
case BootstrapState.askUseExistingSsss:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2022-01-29 12:35:03 +01:00
(_) => bootstrap.useExistingSsss(!_wipe!),
2021-02-06 12:55:27 +01:00
);
break;
case BootstrapState.askUnlockSsss:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2021-07-24 11:45:27 +02:00
(_) => bootstrap.unlockedSsss(),
);
break;
2021-02-06 12:55:27 +01:00
case BootstrapState.askNewSsss:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2021-02-06 12:55:27 +01:00
(_) => bootstrap.newSsss(),
);
break;
case BootstrapState.openExistingSsss:
_recoveryKeyStored = true;
2021-10-14 16:56:08 +02:00
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: IconButton(
2021-10-14 18:09:30 +02:00
icon: const Icon(Icons.close),
2021-10-14 16:56:08 +02:00
onPressed: Navigator.of(context).pop,
2021-10-10 11:40:08 +02:00
),
2023-02-17 09:34:23 +01:00
title: Text(L10n.of(context)!.chatBackup),
2021-10-14 16:56:08 +02:00
),
body: Center(
child: ConstrainedBox(
2021-10-17 14:15:29 +02:00
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
2021-10-14 16:56:08 +02:00
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 8.0),
trailing: Icon(
Icons.info_outlined,
color: Theme.of(context).colorScheme.primary,
),
subtitle: Text(
L10n.of(context)!.pleaseEnterRecoveryKeyDescription,
),
2021-11-19 09:12:47 +01:00
),
const Divider(height: 32),
2021-10-14 16:56:08 +02:00
TextField(
2023-02-17 09:34:23 +01:00
minLines: 1,
2022-12-30 08:39:58 +01:00
maxLines: 2,
2021-10-14 16:56:08 +02:00
autocorrect: false,
2021-11-19 14:42:34 +01:00
readOnly: _recoveryKeyInputLoading,
2021-10-14 16:56:08 +02:00
autofillHints: _recoveryKeyInputLoading
? null
: [AutofillHints.password],
controller: _recoveryKeyTextEditingController,
2023-02-17 09:34:23 +01:00
style: const TextStyle(fontFamily: 'RobotoMono'),
2021-10-14 16:56:08 +02:00
decoration: InputDecoration(
2023-02-17 09:34:23 +01:00
contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle(
fontFamily:
Theme.of(context).textTheme.bodyLarge?.fontFamily,
),
2023-02-17 09:34:23 +01:00
hintText: L10n.of(context)!.recoveryKey,
2021-10-14 16:56:08 +02:00
errorText: _recoveryKeyInputError,
),
2021-10-10 13:35:16 +02:00
),
2021-10-14 16:56:08 +02:00
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).primaryColor,
2021-10-14 16:56:08 +02:00
),
icon: _recoveryKeyInputLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.lock_open_outlined),
label: Text(L10n.of(context)!.unlockOldMessages),
onPressed: _recoveryKeyInputLoading
? null
: () async {
setState(() {
_recoveryKeyInputError = null;
_recoveryKeyInputLoading = true;
});
try {
final key =
_recoveryKeyTextEditingController.text;
await bootstrap.newSsssKey!.unlock(
keyOrPassphrase: key,
);
Logs().d('SSSS unlocked');
await bootstrap.client.encryption!.crossSigning
.selfSign(
keyOrPassphrase: key,
);
Logs().d('Successful elfsigned');
await bootstrap.openExistingSsss();
} catch (e, s) {
Logs().w('Unable to unlock SSSS', e, s);
setState(
() => _recoveryKeyInputError =
L10n.of(context)!.oopsSomethingWentWrong,
);
} finally {
setState(
() => _recoveryKeyInputLoading = false,
);
}
},
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(L10n.of(context)!.or),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 16),
2021-10-14 16:56:08 +02:00
ElevatedButton.icon(
icon: const Icon(Icons.cast_connected_outlined),
2022-01-29 12:35:03 +01:00
label: Text(L10n.of(context)!.transferFromAnotherDevice),
2021-11-19 14:42:34 +01:00
onPressed: _recoveryKeyInputLoading
? null
: () async {
final req = await showFutureLoadingDialog(
context: context,
2022-01-29 12:35:03 +01:00
future: () => widget.client
.userDeviceKeys[widget.client.userID!]!
2021-11-19 14:42:34 +01:00
.startVerification(),
);
if (req.error != null) return;
2022-01-29 12:35:03 +01:00
await KeyVerificationDialog(request: req.result!)
2021-11-19 14:42:34 +01:00
.show(context);
Navigator.of(context, rootNavigator: false).pop();
},
2021-10-10 13:35:16 +02:00
),
2021-10-14 16:56:08 +02:00
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
2022-08-31 19:54:22 +02:00
foregroundColor: Colors.red,
2021-10-14 16:56:08 +02:00
),
2021-10-14 18:09:30 +02:00
icon: const Icon(Icons.delete_outlined),
label: Text(L10n.of(context)!.recoveryKeyLost),
2021-11-19 14:42:34 +01:00
onPressed: _recoveryKeyInputLoading
? null
: () async {
if (OkCancelResult.ok ==
await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.recoveryKeyLost,
2022-01-29 12:35:03 +01:00
message: L10n.of(context)!.wipeChatBackup,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
2021-11-19 14:42:34 +01:00
isDestructiveAction: true,
)) {
setState(() => _createBootstrap(true));
}
},
2021-10-14 16:56:08 +02:00
)
],
),
2021-10-10 13:35:16 +02:00
),
2021-10-10 11:40:08 +02:00
),
);
2021-02-06 12:55:27 +01:00
case BootstrapState.askWipeCrossSigning:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2022-01-29 12:35:03 +01:00
(_) => bootstrap.wipeCrossSigning(_wipe!),
2021-02-06 12:55:27 +01:00
);
break;
case BootstrapState.askSetupCrossSigning:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2021-02-06 12:55:27 +01:00
(_) => bootstrap.askSetupCrossSigning(
setupMasterKey: true,
setupSelfSigningKey: true,
setupUserSigningKey: true,
),
);
break;
case BootstrapState.askWipeOnlineKeyBackup:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2022-01-29 12:35:03 +01:00
(_) => bootstrap.wipeOnlineKeyBackup(_wipe!),
2021-02-06 12:55:27 +01:00
);
break;
case BootstrapState.askSetupOnlineKeyBackup:
2022-05-12 11:25:34 +02:00
WidgetsBinding.instance.addPostFrameCallback(
2021-02-06 12:55:27 +01:00
(_) => bootstrap.askSetupOnlineKeyBackup(true),
);
break;
case BootstrapState.error:
2022-01-29 12:35:03 +01:00
titleText = L10n.of(context)!.oopsSomethingWentWrong;
2021-10-14 18:09:30 +02:00
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
buttons.add(
AdaptiveFlatButton(
label: L10n.of(context)!.close,
onPressed: () =>
Navigator.of(context, rootNavigator: false).pop<bool>(false),
),
);
2021-02-06 12:55:27 +01:00
break;
case BootstrapState.done:
2022-01-29 12:35:03 +01:00
titleText = L10n.of(context)!.everythingReady;
2021-06-22 17:56:15 +02:00
body = Column(
mainAxisSize: MainAxisSize.min,
children: [
2021-10-10 11:40:08 +02:00
Image.asset('assets/backup.png', fit: BoxFit.contain),
2022-01-29 12:35:03 +01:00
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
2021-06-22 17:56:15 +02:00
],
2021-02-06 12:55:27 +01:00
);
buttons.add(
AdaptiveFlatButton(
label: L10n.of(context)!.close,
onPressed: () =>
Navigator.of(context, rootNavigator: false).pop<bool>(false),
),
);
2021-02-06 12:55:27 +01:00
break;
}
}
2022-01-29 12:35:03 +01:00
final title = Text(titleText!);
if (PlatformInfos.isCupertinoStyle) {
return CupertinoAlertDialog(
title: title,
content: body,
actions: buttons,
);
}
return AlertDialog(
title: title,
content: body,
actions: buttons,
);
}
}