feat: Redesign bootsstrap and offer secure storage support

This commit is contained in:
Christian Pauly 2022-07-08 09:51:29 +02:00
parent 091958be0b
commit 2b9bec4e87
3 changed files with 124 additions and 69 deletions

View File

@ -413,8 +413,6 @@
},
"yourUserId": "Your user ID:",
"@yourUserId": {},
"setupChatBackup": "Set up chat backup",
"@setupChatBackup": {},
"iWroteDownTheKey": "I wrote down the key",
"@iWroteDownTheKey": {},
"yourChatBackupHasBeenSetUp": "Your chat backup has been set up.",
@ -424,9 +422,9 @@
"type": "text",
"placeholders": {}
},
"setupChatBackupDescription": "To protect your messages, we have generated a security key for you.\nPlease keep this in a safe place, such as a password manager.",
"setupChatBackupDescription": "To protect your messages, we have generated a recovery key for you.\nPlease keep this in a safe place, such as a password manager.",
"@setupChatBackupDescription": {},
"chatBackupDescription": "Your chat backup is secured with a security key. Please make sure you don't lose it.",
"chatBackupDescription": "Your old messages are secured with a recovery key. Please make sure you don't lose it.",
"@chatBackupDescription": {
"type": "text",
"placeholders": {}
@ -1727,11 +1725,7 @@
"type": "text",
"placeholders": {}
},
"pleaseEnterSecurityKey": "Please enter your security key:",
"@pleaseEnterSecurityKey": {
"type": "text",
"placeholders": {}
},
"pleaseEnterRecoveryKey": "Please enter your recovery key:",
"pleaseEnterYourPassword": "Please enter your password",
"@pleaseEnterYourPassword": {
"type": "text",
@ -1945,16 +1939,8 @@
"type": "text",
"placeholders": {}
},
"securityKey": "Security key",
"@securityKey": {
"type": "text",
"placeholders": {}
},
"securityKeyLost": "Security key lost?",
"@securityKeyLost": {
"type": "text",
"placeholders": {}
},
"recoveryKey": "Recovery key",
"recoveryKeyLost": "Recovery key lost?",
"seenByUser": "Seen by {username}",
"@seenByUser": {
"type": "text",
@ -2578,7 +2564,7 @@
"type": "text",
"placeholders": {}
},
"wipeChatBackup": "Wipe your chat backup to create a new security key?",
"wipeChatBackup": "Wipe your chat backup to create a new recovery key?",
"@wipeChatBackup": {
"type": "text",
"placeholders": {}
@ -2675,10 +2661,8 @@
"@start": {},
"setupChatBackupNow": "Set up your chat backup now",
"@setupChatBackupNow": {},
"pleaseEnterSecurityKeyDescription": "To unlock your chat backup, please enter your security key that has been generated in a previous session. Your security key is NOT your password.",
"@pleaseEnterSecurityKeyDescription": {},
"saveTheSecurityKeyNow": "Save the security key now",
"@saveTheSecurityKeyNow": {},
"pleaseEnterRecoveryKeyDescription": "To unlock your old messages, please enter your recovery key that has been generated in a previous session. Your recovery key is NOT your password.",
"saveTheRecoveryKeyNow": "Save the recovery key now",
"addToStory": "Add to story",
"@addToStory": {},
"publish": "Publish",
@ -2828,5 +2812,12 @@
},
"noEmailWarning": "Please enter a valid email address. Otherwise you won't be able to reset your password. If you don't want to, tap again on the button to continue.",
"stories": "Stories",
"users": "Users"
"users": "Users",
"enableAutoBackups": "Enable auto backups",
"unlockOldMessages": "Unlock old messages",
"storeInSecureStorageDescription": "Store the recovery key in the secure storage of this device.",
"saveKeyManuallyDescription": "Save this key manually by triggering the system share dialog or clipboard.",
"storeInAndroidKeystore": "Store in Android KeyStore",
"storeInAppleKeyChain": "Store in Apple KeyChain",
"storeSecurlyOnThisDevice": "Store securly on this device"
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/encryption.dart';
import 'package:matrix/encryption/utils/bootstrap.dart';
@ -56,8 +57,32 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
bool _recoveryKeyStored = false;
bool _recoveryKeyCopied = false;
bool? _storeInSecureStorage = false;
bool? _wipe;
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;
}
static const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
@override
void initState() {
_createBootstrap(widget.wipe);
@ -70,6 +95,10 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
_recoveryKeyStored = false;
bootstrap =
widget.client.encryption!.bootstrap(onUpdate: () => setState(() {}));
secureStorage.read(key: _secureStorageKey).then((key) {
if (key == null) return;
_recoveryKeyTextEditingController.text = key;
});
}
@override
@ -84,7 +113,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
if (bootstrap.newSsssKey?.recoveryKey != null &&
_recoveryKeyStored == false) {
final key = bootstrap.newSsssKey!.recoveryKey;
titleText = L10n.of(context)!.securityKey;
titleText = L10n.of(context)!.recoveryKey;
return Scaffold(
appBar: AppBar(
centerTitle: true,
@ -92,7 +121,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
title: Text(L10n.of(context)!.securityKey),
title: Text(L10n.of(context)!.recoveryKey),
),
body: Center(
child: ConstrainedBox(
@ -101,15 +130,18 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text(
L10n.of(context)!.chatBackupDescription,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
trailing: Icon(
Icons.info_outlined,
color: Theme.of(context).colorScheme.primary,
),
subtitle: Text(L10n.of(context)!.chatBackupDescription),
),
const Divider(
height: 32,
thickness: 1,
),
const Divider(height: 64),
TextField(
minLines: 4,
maxLines: 4,
@ -117,10 +149,26 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
controller: TextEditingController(text: key),
),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Icons.save_alt_outlined),
label: Text(L10n.of(context)!.saveTheSecurityKeyNow),
onPressed: () {
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) {
final box = context.findRenderObject() as RenderBox;
Share.share(
key!,
@ -129,18 +177,25 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
);
setState(() => _recoveryKeyCopied = true);
},
title: Text(L10n.of(context)!.copyToClipboard),
subtitle: Text(L10n.of(context)!.saveKeyManuallyDescription),
),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Theme.of(context).primaryColor,
),
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.next),
onPressed: _recoveryKeyCopied
? () => setState(() => _recoveryKeyStored = true)
: null,
onPressed:
(_recoveryKeyCopied || _storeInSecureStorage == true)
? () {
if (_storeInSecureStorage == true) {
secureStorage.write(
key: _secureStorageKey,
value: key,
);
}
setState(() => _recoveryKeyStored = true);
}
: null,
),
],
),
@ -185,7 +240,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
title: Text(L10n.of(context)!.pleaseEnterSecurityKey),
title: Text(L10n.of(context)!.unlockOldMessages),
),
body: Center(
child: ConstrainedBox(
@ -194,15 +249,17 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text(
L10n.of(context)!.pleaseEnterSecurityKeyDescription,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
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),
),
const Divider(height: 64),
const Divider(height: 32),
TextField(
minLines: 1,
maxLines: 1,
@ -214,7 +271,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
controller: _recoveryKeyTextEditingController,
decoration: InputDecoration(
hintText: 'Abc123 Def456',
labelText: L10n.of(context)!.securityKey,
labelText: L10n.of(context)!.recoveryKey,
errorText: _recoveryKeyInputError,
),
),
@ -223,7 +280,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: _recoveryKeyInputLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.lock_open_outlined),
label: Text(L10n.of(context)!.unlockChatBackup),
label: Text(L10n.of(context)!.unlockOldMessages),
onPressed: _recoveryKeyInputLoading
? null
: () async {
@ -254,7 +311,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
() => _recoveryKeyInputLoading = false);
}
}),
const SizedBox(height: 32),
const SizedBox(height: 16),
Row(children: [
const Expanded(child: Divider()),
Padding(
@ -263,12 +320,8 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
),
const Expanded(child: Divider()),
]),
const SizedBox(height: 32),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Theme.of(context).primaryColor,
),
icon: const Icon(Icons.cast_connected_outlined),
label: Text(L10n.of(context)!.transferFromAnotherDevice),
onPressed: _recoveryKeyInputLoading
@ -289,11 +342,10 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Colors.red,
),
icon: const Icon(Icons.delete_outlined),
label: Text(L10n.of(context)!.securityKeyLost),
label: Text(L10n.of(context)!.recoveryKeyLost),
onPressed: _recoveryKeyInputLoading
? null
: () async {
@ -301,7 +353,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.securityKeyLost,
title: L10n.of(context)!.recoveryKeyLost,
message: L10n.of(context)!.wipeChatBackup,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,

View File

@ -168,13 +168,25 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: Image.asset(
'assets/backup.png',
fit: BoxFit.contain,
width: 44,
leading: CircleAvatar(
radius: Avatar.defaultSize / 2,
child:
const Icon(Icons.enhanced_encryption_outlined),
backgroundColor: Theme.of(context)
.colorScheme
.secondaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
title: Text(
L10n.of(context)!.setupChatBackupNow,
Matrix.of(context)
.client
.encryption!
.keyManager
.enabled
? L10n.of(context)!.unlockOldMessages
: L10n.of(context)!.enableAutoBackups,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),