diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 74168531..3cf406f1 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -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" } diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 6e35e62e..2c05ce42 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -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 { 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 { _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 { 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 { 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 { 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 { 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 { ); 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 { 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 { 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 { 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 { 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 { () => _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 { ), 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 { 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 { 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, diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 77a0034f..9a988117 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -168,13 +168,25 @@ class _ChatListViewBodyState extends State { 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, ),