mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-27 23:09:35 +01:00
feat: Redesign bootsstrap and offer secure storage support
This commit is contained in:
parent
091958be0b
commit
2b9bec4e87
@ -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"
|
||||
}
|
||||
|
@ -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,17 +177,24 @@ 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)
|
||||
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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user