mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-24 04:59:26 +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": "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"
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user