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": "Your user ID:",
"@yourUserId": {}, "@yourUserId": {},
"setupChatBackup": "Set up chat backup",
"@setupChatBackup": {},
"iWroteDownTheKey": "I wrote down the key", "iWroteDownTheKey": "I wrote down the key",
"@iWroteDownTheKey": {}, "@iWroteDownTheKey": {},
"yourChatBackupHasBeenSetUp": "Your chat backup has been set up.", "yourChatBackupHasBeenSetUp": "Your chat backup has been set up.",
@ -424,9 +422,9 @@
"type": "text", "type": "text",
"placeholders": {} "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": {}, "@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": { "@chatBackupDescription": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -1727,11 +1725,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"pleaseEnterSecurityKey": "Please enter your security key:", "pleaseEnterRecoveryKey": "Please enter your recovery key:",
"@pleaseEnterSecurityKey": {
"type": "text",
"placeholders": {}
},
"pleaseEnterYourPassword": "Please enter your password", "pleaseEnterYourPassword": "Please enter your password",
"@pleaseEnterYourPassword": { "@pleaseEnterYourPassword": {
"type": "text", "type": "text",
@ -1945,16 +1939,8 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"securityKey": "Security key", "recoveryKey": "Recovery key",
"@securityKey": { "recoveryKeyLost": "Recovery key lost?",
"type": "text",
"placeholders": {}
},
"securityKeyLost": "Security key lost?",
"@securityKeyLost": {
"type": "text",
"placeholders": {}
},
"seenByUser": "Seen by {username}", "seenByUser": "Seen by {username}",
"@seenByUser": { "@seenByUser": {
"type": "text", "type": "text",
@ -2578,7 +2564,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"wipeChatBackup": "Wipe your chat backup to create a new security key?", "wipeChatBackup": "Wipe your chat backup to create a new recovery key?",
"@wipeChatBackup": { "@wipeChatBackup": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -2675,10 +2661,8 @@
"@start": {}, "@start": {},
"setupChatBackupNow": "Set up your chat backup now", "setupChatBackupNow": "Set up your chat backup now",
"@setupChatBackupNow": {}, "@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.", "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.",
"@pleaseEnterSecurityKeyDescription": {}, "saveTheRecoveryKeyNow": "Save the recovery key now",
"saveTheSecurityKeyNow": "Save the security key now",
"@saveTheSecurityKeyNow": {},
"addToStory": "Add to story", "addToStory": "Add to story",
"@addToStory": {}, "@addToStory": {},
"publish": "Publish", "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.", "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", "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:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.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:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/encryption.dart'; import 'package:matrix/encryption.dart';
import 'package:matrix/encryption/utils/bootstrap.dart'; import 'package:matrix/encryption/utils/bootstrap.dart';
@ -56,8 +57,32 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
bool _recoveryKeyStored = false; bool _recoveryKeyStored = false;
bool _recoveryKeyCopied = false; bool _recoveryKeyCopied = false;
bool? _storeInSecureStorage = false;
bool? _wipe; 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 @override
void initState() { void initState() {
_createBootstrap(widget.wipe); _createBootstrap(widget.wipe);
@ -70,6 +95,10 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
_recoveryKeyStored = false; _recoveryKeyStored = false;
bootstrap = bootstrap =
widget.client.encryption!.bootstrap(onUpdate: () => setState(() {})); widget.client.encryption!.bootstrap(onUpdate: () => setState(() {}));
secureStorage.read(key: _secureStorageKey).then((key) {
if (key == null) return;
_recoveryKeyTextEditingController.text = key;
});
} }
@override @override
@ -84,7 +113,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
if (bootstrap.newSsssKey?.recoveryKey != null && if (bootstrap.newSsssKey?.recoveryKey != null &&
_recoveryKeyStored == false) { _recoveryKeyStored == false) {
final key = bootstrap.newSsssKey!.recoveryKey; final key = bootstrap.newSsssKey!.recoveryKey;
titleText = L10n.of(context)!.securityKey; titleText = L10n.of(context)!.recoveryKey;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
@ -92,7 +121,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop, onPressed: Navigator.of(context).pop,
), ),
title: Text(L10n.of(context)!.securityKey), title: Text(L10n.of(context)!.recoveryKey),
), ),
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
@ -101,15 +130,18 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
child: ListView( child: ListView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: [ children: [
Text( ListTile(
L10n.of(context)!.chatBackupDescription, contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
textAlign: TextAlign.center, trailing: Icon(
style: const TextStyle( Icons.info_outlined,
fontSize: 16, color: Theme.of(context).colorScheme.primary,
fontStyle: FontStyle.italic,
), ),
subtitle: Text(L10n.of(context)!.chatBackupDescription),
),
const Divider(
height: 32,
thickness: 1,
), ),
const Divider(height: 64),
TextField( TextField(
minLines: 4, minLines: 4,
maxLines: 4, maxLines: 4,
@ -117,10 +149,26 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
controller: TextEditingController(text: key), controller: TextEditingController(text: key),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( if (_supportsSecureStorage)
icon: const Icon(Icons.save_alt_outlined), CheckboxListTile(
label: Text(L10n.of(context)!.saveTheSecurityKeyNow), contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
onPressed: () { 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; final box = context.findRenderObject() as RenderBox;
Share.share( Share.share(
key!, key!,
@ -129,18 +177,25 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
); );
setState(() => _recoveryKeyCopied = true); setState(() => _recoveryKeyCopied = true);
}, },
title: Text(L10n.of(context)!.copyToClipboard),
subtitle: Text(L10n.of(context)!.saveKeyManuallyDescription),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Theme.of(context).primaryColor,
),
icon: const Icon(Icons.check_outlined), icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.next), label: Text(L10n.of(context)!.next),
onPressed: _recoveryKeyCopied onPressed:
? () => setState(() => _recoveryKeyStored = true) (_recoveryKeyCopied || _storeInSecureStorage == true)
: null, ? () {
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), icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop, onPressed: Navigator.of(context).pop,
), ),
title: Text(L10n.of(context)!.pleaseEnterSecurityKey), title: Text(L10n.of(context)!.unlockOldMessages),
), ),
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
@ -194,15 +249,17 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
child: ListView( child: ListView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: [ children: [
Text( ListTile(
L10n.of(context)!.pleaseEnterSecurityKeyDescription, contentPadding:
textAlign: TextAlign.center, const EdgeInsets.symmetric(horizontal: 8.0),
style: const TextStyle( trailing: Icon(
fontSize: 16, Icons.info_outlined,
fontStyle: FontStyle.italic, color: Theme.of(context).colorScheme.primary,
), ),
subtitle: Text(
L10n.of(context)!.pleaseEnterRecoveryKeyDescription),
), ),
const Divider(height: 64), const Divider(height: 32),
TextField( TextField(
minLines: 1, minLines: 1,
maxLines: 1, maxLines: 1,
@ -214,7 +271,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
controller: _recoveryKeyTextEditingController, controller: _recoveryKeyTextEditingController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Abc123 Def456', hintText: 'Abc123 Def456',
labelText: L10n.of(context)!.securityKey, labelText: L10n.of(context)!.recoveryKey,
errorText: _recoveryKeyInputError, errorText: _recoveryKeyInputError,
), ),
), ),
@ -223,7 +280,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: _recoveryKeyInputLoading icon: _recoveryKeyInputLoading
? const CircularProgressIndicator.adaptive() ? const CircularProgressIndicator.adaptive()
: const Icon(Icons.lock_open_outlined), : const Icon(Icons.lock_open_outlined),
label: Text(L10n.of(context)!.unlockChatBackup), label: Text(L10n.of(context)!.unlockOldMessages),
onPressed: _recoveryKeyInputLoading onPressed: _recoveryKeyInputLoading
? null ? null
: () async { : () async {
@ -254,7 +311,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
() => _recoveryKeyInputLoading = false); () => _recoveryKeyInputLoading = false);
} }
}), }),
const SizedBox(height: 32), const SizedBox(height: 16),
Row(children: [ Row(children: [
const Expanded(child: Divider()), const Expanded(child: Divider()),
Padding( Padding(
@ -263,12 +320,8 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
), ),
const Expanded(child: Divider()), const Expanded(child: Divider()),
]), ]),
const SizedBox(height: 32), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Theme.of(context).primaryColor,
),
icon: const Icon(Icons.cast_connected_outlined), icon: const Icon(Icons.cast_connected_outlined),
label: Text(L10n.of(context)!.transferFromAnotherDevice), label: Text(L10n.of(context)!.transferFromAnotherDevice),
onPressed: _recoveryKeyInputLoading onPressed: _recoveryKeyInputLoading
@ -289,11 +342,10 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Colors.red, onPrimary: Colors.red,
), ),
icon: const Icon(Icons.delete_outlined), icon: const Icon(Icons.delete_outlined),
label: Text(L10n.of(context)!.securityKeyLost), label: Text(L10n.of(context)!.recoveryKeyLost),
onPressed: _recoveryKeyInputLoading onPressed: _recoveryKeyInputLoading
? null ? null
: () async { : () async {
@ -301,7 +353,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
await showOkCancelAlertDialog( await showOkCancelAlertDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
title: L10n.of(context)!.securityKeyLost, title: L10n.of(context)!.recoveryKeyLost,
message: L10n.of(context)!.wipeChatBackup, message: L10n.of(context)!.wipeChatBackup,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,

View File

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