From 4550686829eb8748fd8ce6d2d06029cff00fbbe0 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 25 Jun 2020 14:29:06 +0000 Subject: [PATCH] Add Cross-Signing --- CHANGELOG.md | 4 + lib/components/list_items/message.dart | 3 +- lib/components/matrix.dart | 33 ++- lib/l10n/intl_messages.arb | 206 +++++++++++++- lib/l10n/l10n.dart | 122 +++++++++ lib/l10n/messages_messages.dart | 72 ++++- lib/utils/famedlysdk_store.dart | 2 +- lib/utils/room_status_extension.dart | 11 +- lib/views/chat_encryption_settings.dart | 198 +++++++++++--- lib/views/key_verification.dart | 347 ++++++++++++++++++++++++ lib/views/settings.dart | 165 ++++++++++- pubspec.lock | 58 +++- pubspec.yaml | 2 +- 13 files changed, 1149 insertions(+), 74 deletions(-) create mode 100644 lib/views/key_verification.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a229164..74ce0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ - Chat app bar transparent - Implement web file picker - Minor design and UX improvements +- Implement Cross Signing +- Restore keys from online key backup ### Changes: - Show presences of users sharing a direct chat - Big refactoring +### Fixes: +- Various fixes, including e2ee fixes and olm session recovery # Version 0.14.0 - 2020-05-20 ### Features: diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index cbbdab7..35e064d 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -1,6 +1,5 @@ import 'package:bubble/bubble.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:famedlysdk/encryption.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/components/message_content.dart'; import 'package:fluffychat/components/reply_content.dart'; @@ -122,7 +121,7 @@ class Message extends StatelessWidget { ), if (event.type == EventTypes.Encrypted && event.messageType == MessageTypes.BadEncrypted && - event.content['body'] == DecryptError.UNKNOWN_SESSION) + event.content['can_request_session'] == true) RaisedButton( color: color.withAlpha(100), child: Text( diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index dc91e0c..93bc680 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -14,6 +14,8 @@ import '../l10n/l10n.dart'; import '../utils/beautify_string_extension.dart'; import '../utils/famedlysdk_store.dart'; import 'avatar.dart'; +import '../views/key_verification.dart'; +import '../utils/app_route.dart'; class Matrix extends StatefulWidget { static const String callNamespace = 'chat.fluffy.jitsi_call'; @@ -97,6 +99,7 @@ class MatrixState extends State { }; StreamSubscription onRoomKeyRequestSub; + StreamSubscription onKeyVerificationRequestSub; StreamSubscription onJitsiCallSub; void onJitsiCall(EventUpdate eventUpdate) { @@ -159,7 +162,17 @@ class MatrixState extends State { store = widget.store ?? Store(); if (widget.client == null) { debugPrint('[Matrix] Init matrix client'); - client = Client(widget.clientName, debug: false); + final Set verificationMethods = { + KeyVerificationMethod.numbers + }; + if (!kIsWeb) { + // emojis don't show in web somehow + verificationMethods.add(KeyVerificationMethod.emoji); + } + client = Client(widget.clientName, + debug: false, + enableE2eeRecovery: true, + verificationMethods: verificationMethods); onJitsiCallSub ??= client.onEvent.stream .where((e) => e.type == 'timeline' && @@ -184,6 +197,23 @@ class MatrixState extends State { await request.forwardKey(); } }); + onKeyVerificationRequestSub ??= client.onKeyVerificationRequest.stream + .listen((KeyVerification request) async { + if (await SimpleDialogs(context).askConfirmation( + titleText: L10n.of(context).newVerificationRequest, + contentText: L10n.of(context).askVerificationRequest(request.userId), + )) { + await request.acceptVerification(); + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: request), + ), + ); + } else { + await request.rejectVerification(); + } + }); _initWithStore(); } else { client = widget.client; @@ -210,6 +240,7 @@ class MatrixState extends State { @override void dispose() { onRoomKeyRequestSub?.cancel(); + onKeyVerificationRequestSub?.cancel(); onJitsiCallSub?.cancel(); super.dispose(); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index a8edc60..60da258 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,10 +1,15 @@ { - "@@last_modified": "2020-05-15T15:34:50.065646", + "@@last_modified": "2020-06-25T16:02:16.297192", "About": "About", "@About": { "type": "text", "placeholders": {} }, + "Accept": "Accept", + "@Accept": { + "type": "text", + "placeholders": {} + }, "acceptedTheInvitation": "{username} accepted the invitation", "@acceptedTheInvitation": { "type": "text", @@ -74,6 +79,28 @@ "type": "text", "placeholders": {} }, + "askSSSSCache": "Please enter your secure store passphrase or recovery key to cache the keys.", + "@askSSSSCache": { + "type": "text", + "placeholders": {} + }, + "askSSSSSign": "To be able to sign the other person, please enter your secure store passphrase or recovery key.", + "@askSSSSSign": { + "type": "text", + "placeholders": {} + }, + "askSSSSVerify": "Please enter your secure store passphrase or recovery key to verify your session.", + "@askSSSSVerify": { + "type": "text", + "placeholders": {} + }, + "askVerificationRequest": "Accept this verification request from {username}?", + "@askVerificationRequest": { + "type": "text", + "placeholders": { + "username": {} + } + }, "Authentication": "Authentication", "@Authentication": { "type": "text", @@ -102,6 +129,11 @@ "targetName": {} } }, + "Block Device": "Block Device", + "@Block Device": { + "type": "text", + "placeholders": {} + }, "byDefaultYouWillBeConnectedTo": "By default you will be connected to {homeserver}", "@byDefaultYouWillBeConnectedTo": { "type": "text", @@ -109,6 +141,11 @@ "homeserver": {} } }, + "cachedKeys": "Successfully cached keys!", + "@cachedKeys": { + "type": "text", + "placeholders": {} + }, "Cancel": "Cancel", "@Cancel": { "type": "text", @@ -273,6 +310,16 @@ "type": "text", "placeholders": {} }, + "compareEmojiMatch": "Compare and make sure the following emoji match those of the other device:", + "@compareEmojiMatch": { + "type": "text", + "placeholders": {} + }, + "compareNumbersMatch": "Compare and make sure the following numbers match those of the other device:", + "@compareNumbersMatch": { + "type": "text", + "placeholders": {} + }, "Confirm": "Confirm", "@Confirm": { "type": "text", @@ -354,6 +401,16 @@ "type": "text", "placeholders": {} }, + "crossSigningDisabled": "Cross-Signing is disabled", + "@crossSigningDisabled": { + "type": "text", + "placeholders": {} + }, + "crossSigningEnabled": "Cross-Signing is enabled", + "@crossSigningEnabled": { + "type": "text", + "placeholders": {} + }, "Currently active": "Currently active", "@Currently active": { "type": "text", @@ -464,6 +521,11 @@ "type": "text", "placeholders": {} }, + "Encryption": "Encryption", + "@Encryption": { + "type": "text", + "placeholders": {} + }, "Encryption algorithm": "Encryption algorithm", "@Encryption algorithm": { "type": "text", @@ -594,6 +656,11 @@ "type": "text", "placeholders": {} }, + "incorrectPassphraseOrKey": "Incorrect passphrase or recovery key", + "@incorrectPassphraseOrKey": { + "type": "text", + "placeholders": {} + }, "Invite contact": "Invite contact", "@Invite contact": { "type": "text", @@ -632,6 +699,11 @@ "type": "text", "placeholders": {} }, + "isDeviceKeyCorrect": "Is the following device key correct?", + "@isDeviceKeyCorrect": { + "type": "text", + "placeholders": {} + }, "is typing...": "is typing...", "@is typing...": { "type": "text", @@ -649,6 +721,16 @@ "username": {} } }, + "keysCached": "Keys are cached", + "@keysCached": { + "type": "text", + "placeholders": {} + }, + "keysMissing": "Keys are missing", + "@keysMissing": { + "type": "text", + "placeholders": {} + }, "kicked": "{username} kicked {targetName}", "@kicked": { "type": "text", @@ -788,6 +870,21 @@ "type": "text", "placeholders": {} }, + "newVerificationRequest": "New verification request!", + "@newVerificationRequest": { + "type": "text", + "placeholders": {} + }, + "noCrossSignBootstrap": "Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot.", + "@noCrossSignBootstrap": { + "type": "text", + "placeholders": {} + }, + "noMegolmBootstrap": "Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot.", + "@noMegolmBootstrap": { + "type": "text", + "placeholders": {} + }, "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/", "@It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": { "type": "text", @@ -830,6 +927,16 @@ "type": "text", "placeholders": {} }, + "onlineKeyBackupDisabled": "Online Key Backup is disabled", + "@onlineKeyBackupDisabled": { + "type": "text", + "placeholders": {} + }, + "onlineKeyBackupEnabled": "Online Key Backup is enabled", + "@onlineKeyBackupEnabled": { + "type": "text", + "placeholders": {} + }, "Oops something went wrong...": "Oops something went wrong...", "@Oops something went wrong...": { "type": "text", @@ -855,6 +962,11 @@ "type": "text", "placeholders": {} }, + "passphraseOrKey": "passphrase or recovery key", + "@passphraseOrKey": { + "type": "text", + "placeholders": {} + }, "Password": "Password", "@Password": { "type": "text", @@ -897,6 +1009,11 @@ "type": "text", "placeholders": {} }, + "Reject": "Reject", + "@Reject": { + "type": "text", + "placeholders": {} + }, "Rejoin": "Rejoin", "@Rejoin": { "type": "text", @@ -978,6 +1095,11 @@ "type": "text", "placeholders": {} }, + "Room has been upgraded": "Room has been upgraded", + "@Room has been upgraded": { + "type": "text", + "placeholders": {} + }, "Saturday": "Saturday", "@Saturday": { "type": "text", @@ -1083,6 +1205,11 @@ "username": {} } }, + "sessionVerified": "Session is verified", + "@sessionVerified": { + "type": "text", + "placeholders": {} + }, "Set a profile picture": "Set a profile picture", "@Set a profile picture": { "type": "text", @@ -1113,6 +1240,11 @@ "type": "text", "placeholders": {} }, + "Skip": "Skip", + "@Skip": { + "type": "text", + "placeholders": {} + }, "Change your style": "Change your style", "@Change your style": { "type": "text", @@ -1153,6 +1285,11 @@ "type": "text", "placeholders": {} }, + "Submit": "Submit", + "@Submit": { + "type": "text", + "placeholders": {} + }, "Sunday": "Sunday", "@Sunday": { "type": "text", @@ -1168,6 +1305,16 @@ "type": "text", "placeholders": {} }, + "They Don't Match": "They Don't Match", + "@They Don't Match": { + "type": "text", + "placeholders": {} + }, + "They Match": "They Match", + "@They Match": { + "type": "text", + "placeholders": {} + }, "This room has been archived.": "This room has been archived.", "@This room has been archived.": { "type": "text", @@ -1212,6 +1359,11 @@ "targetName": {} } }, + "Unblock Device": "Unblock Device", + "@Unblock Device": { + "type": "text", + "placeholders": {} + }, "Unmute chat": "Unmute chat", "@Unmute chat": { "type": "text", @@ -1227,6 +1379,11 @@ "type": "text", "placeholders": {} }, + "unknownSessionVerify": "Unknown session, please verify", + "@unknownSessionVerify": { + "type": "text", + "placeholders": {} + }, "unknownEvent": "Unknown event '{type}'", "@unknownEvent": { "type": "text", @@ -1297,6 +1454,36 @@ "type": "text", "placeholders": {} }, + "verifyManual": "Verify Manually", + "@verifyManual": { + "type": "text", + "placeholders": {} + }, + "verifiedSession": "Successfully verified session!", + "@verifiedSession": { + "type": "text", + "placeholders": {} + }, + "verifyStart": "Start Verification", + "@verifyStart": { + "type": "text", + "placeholders": {} + }, + "verifySuccess": "You successfully verified!", + "@verifySuccess": { + "type": "text", + "placeholders": {} + }, + "verifyTitle": "Verifying other account", + "@verifyTitle": { + "type": "text", + "placeholders": {} + }, + "Verify User": "Verify User", + "@Verify User": { + "type": "text", + "placeholders": {} + }, "Video call": "Video call", "@Video call": { "type": "text", @@ -1322,6 +1509,21 @@ "type": "text", "placeholders": {} }, + "waitingPartnerAcceptRequest": "Waiting for partner to accept the request...", + "@waitingPartnerAcceptRequest": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerEmoji": "Waiting for partner to accept the emoji...", + "@waitingPartnerEmoji": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerNumbers": "Waiting for partner to accept the numbers...", + "@waitingPartnerNumbers": { + "type": "text", + "placeholders": {} + }, "Wallpaper": "Wallpaper", "@Wallpaper": { "type": "text", @@ -1387,4 +1589,4 @@ "type": "text", "placeholders": {} } -} +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 950145b..7b7a17c 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -46,6 +46,8 @@ class L10n extends MatrixLocalizations { String get about => Intl.message("About"); + String get accept => Intl.message("Accept"); + String acceptedTheInvitation(String username) => Intl.message( "$username accepted the invitation", name: "acceptedTheInvitation", @@ -81,6 +83,22 @@ class L10n extends MatrixLocalizations { String get areYouSure => Intl.message("Are you sure?"); + String get askSSSSCache => Intl.message( + "Please enter your secure store passphrase or recovery key to cache the keys.", + name: "askSSSSCache"); + + String get askSSSSSign => Intl.message( + "To be able to sign the other person, please enter your secure store passphrase or recovery key.", + name: "askSSSSSign"); + + String get askSSSSVerify => Intl.message( + "Please enter your secure store passphrase or recovery key to verify your session.", + name: "askSSSSVerify"); + + String askVerificationRequest(String username) => + Intl.message("Accept this verification request from $username?", + name: "askVerificationRequest", args: [username]); + String get authentication => Intl.message("Authentication"); String get avatarHasBeenChanged => Intl.message("Avatar has been changed"); @@ -95,12 +113,17 @@ class L10n extends MatrixLocalizations { args: [username, targetName], ); + String get blockDevice => Intl.message("Block Device"); + String byDefaultYouWillBeConnectedTo(String homeserver) => Intl.message( 'By default you will be connected to $homeserver', name: 'byDefaultYouWillBeConnectedTo', args: [homeserver], ); + String get cachedKeys => + Intl.message("Successfully cached keys!", name: "cachedKeys"); + String get cancel => Intl.message("Cancel"); String changedTheChatAvatar(String username) => Intl.message( @@ -216,6 +239,14 @@ class L10n extends MatrixLocalizations { String get close => Intl.message("Close"); + String get compareEmojiMatch => Intl.message( + "Compare and make sure the following emoji match those of the other device:", + name: "compareEmojiMatch"); + + String get compareNumbersMatch => Intl.message( + "Compare and make sure the following numbers match those of the other device:", + name: "compareNumbersMatch"); + String get confirm => Intl.message("Confirm"); String get connect => Intl.message('Connect'); @@ -261,6 +292,12 @@ class L10n extends MatrixLocalizations { String get createNewGroup => Intl.message("Create new group"); + String get crossSigningDisabled => + Intl.message("Cross-Signing is disabled", name: "crossSigningDisabled"); + + String get crossSigningEnabled => + Intl.message("Cross-Signing is enabled", name: "crossSigningEnabled"); + String get currentlyActive => Intl.message('Currently active'); String dateAndTimeOfDay(String date, String timeOfDay) => Intl.message( @@ -319,6 +356,8 @@ class L10n extends MatrixLocalizations { String get enableEncryptionWarning => Intl.message( "You won't be able to disable the encryption anymore. Are you sure?"); + String get encryption => Intl.message("Encryption"); + String get encryptionAlgorithm => Intl.message("Encryption algorithm"); String get encryptionNotEnabled => Intl.message("Encryption is not enabled"); @@ -381,6 +420,10 @@ class L10n extends MatrixLocalizations { String get identity => Intl.message("Identity"); + String get incorrectPassphraseOrKey => + Intl.message("Incorrect passphrase or recovery key", + name: "incorrectPassphraseOrKey"); + String get inviteContact => Intl.message("Invite contact"); String inviteContactToGroup(String groupName) => Intl.message( @@ -405,6 +448,10 @@ class L10n extends MatrixLocalizations { String get invitedUsersOnly => Intl.message("Invited users only"); + String get isDeviceKeyCorrect => + Intl.message("Is the following device key correct?", + name: "isDeviceKeyCorrect"); + String get isTyping => Intl.message("is typing..."); String get editJitsiInstance => Intl.message('Edit Jitsi instance'); @@ -415,6 +462,11 @@ class L10n extends MatrixLocalizations { args: [username], ); + String get keysCached => Intl.message("Keys are cached", name: "keysCached"); + + String get keysMissing => + Intl.message("Keys are missing", name: "keysMissing"); + String kicked(String username, String targetName) => Intl.message( "$username kicked $targetName", name: "kicked", @@ -493,6 +545,17 @@ class L10n extends MatrixLocalizations { String get newPrivateChat => Intl.message("New private chat"); + String get newVerificationRequest => + Intl.message("New verification request!", name: "newVerificationRequest"); + + String get noCrossSignBootstrap => Intl.message( + "Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot.", + name: "noCrossSignBootstrap"); + + String get noMegolmBootstrap => Intl.message( + "Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot.", + name: "noMegolmBootstrap"); + String get noGoogleServicesWarning => Intl.message( "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/"); @@ -511,6 +574,14 @@ class L10n extends MatrixLocalizations { String get ok => Intl.message('ok'); + String get onlineKeyBackupDisabled => + Intl.message("Online Key Backup is disabled", + name: "onlineKeyBackupDisabled"); + + String get onlineKeyBackupEnabled => + Intl.message("Online Key Backup is enabled", + name: "onlineKeyBackupEnabled"); + String get oopsSomethingWentWrong => Intl.message("Oops something went wrong..."); @@ -523,6 +594,9 @@ class L10n extends MatrixLocalizations { String get participatingUserDevices => Intl.message("Participating user devices"); + String get passphraseOrKey => + Intl.message("passphrase or recovery key", name: "passphraseOrKey"); + String get password => Intl.message("Password"); String get pickImage => Intl.message('Pick image'); @@ -546,6 +620,8 @@ class L10n extends MatrixLocalizations { String get publicRooms => Intl.message("Public Rooms"); + String get reject => Intl.message("Reject"); + String get rejoin => Intl.message("Rejoin"); String get renderRichContent => Intl.message("Render rich message content"); @@ -662,6 +738,9 @@ class L10n extends MatrixLocalizations { args: [username], ); + String get sessionVerified => + Intl.message("Session is verified", name: "sessionVerified"); + String get setAProfilePicture => Intl.message("Set a profile picture"); String get setGroupDescription => Intl.message("Set group description"); @@ -674,6 +753,8 @@ class L10n extends MatrixLocalizations { String get signUp => Intl.message("Sign up"); + String get skip => Intl.message("Skip"); + String get changeTheme => Intl.message("Change your style"); String get systemTheme => Intl.message("System"); @@ -690,12 +771,18 @@ class L10n extends MatrixLocalizations { String get startYourFirstChat => Intl.message("Start your first chat :-)"); + String get submit => Intl.message("Submit"); + String get sunday => Intl.message("Sunday"); String get donate => Intl.message("Donate"); String get tapToShowMenu => Intl.message("Tap to show menu"); + String get theyDontMatch => Intl.message("They Don't Match"); + + String get theyMatch => Intl.message("They Match"); + String get thisRoomHasBeenArchived => Intl.message("This room has been archived."); @@ -726,6 +813,8 @@ class L10n extends MatrixLocalizations { args: [username, targetName], ); + String get unblockDevice => Intl.message("Unblock Device"); + String get unmuteChat => Intl.message('Unmute chat'); String get unknownDevice => Intl.message("Unknown device"); @@ -733,6 +822,10 @@ class L10n extends MatrixLocalizations { String get unknownEncryptionAlgorithm => Intl.message("Unknown encryption algorithm"); + String get unknownSessionVerify => + Intl.message("Unknown session, please verify", + name: "unknownSessionVerify"); + String unknownEvent(String type) => Intl.message( "Unknown event '$type'", name: "unknownEvent", @@ -787,6 +880,23 @@ class L10n extends MatrixLocalizations { String get verify => Intl.message("Verify"); + String get verifyManual => + Intl.message("Verify Manually", name: "verifyManual"); + + String get verifiedSession => + Intl.message("Successfully verified session!", name: "verifiedSession"); + + String get verifyStart => + Intl.message("Start Verification", name: "verifyStart"); + + String get verifySuccess => + Intl.message("You successfully verified!", name: "verifySuccess"); + + String get verifyTitle => + Intl.message("Verifying other account", name: "verifyTitle"); + + String get verifyUser => Intl.message("Verify User"); + String get videoCall => Intl.message('Video call'); String get visibleForAllParticipants => @@ -799,6 +909,18 @@ class L10n extends MatrixLocalizations { String get voiceMessage => Intl.message("Voice message"); + String get waitingPartnerAcceptRequest => + Intl.message("Waiting for partner to accept the request...", + name: "waitingPartnerAcceptRequest"); + + String get waitingPartnerEmoji => + Intl.message("Waiting for partner to accept the emoji...", + name: "waitingPartnerEmoji"); + + String get waitingPartnerNumbers => + Intl.message("Waiting for partner to accept the numbers...", + name: "waitingPartnerNumbers"); + String get wallpaper => Intl.message("Wallpaper"); String get warningEncryptionInBeta => Intl.message( diff --git a/lib/l10n/messages_messages.dart b/lib/l10n/messages_messages.dart index b859273..5472687 100644 --- a/lib/l10n/messages_messages.dart +++ b/lib/l10n/messages_messages.dart @@ -23,6 +23,8 @@ class MessageLookup extends MessageLookupByLibrary { static m1(username) => "${username} activated end to end encryption"; + static m60(username) => "Accept this verification request from ${username}?"; + static m2(username, targetName) => "${username} banned ${targetName}"; static m3(homeserver) => "By default you will be connected to ${homeserver}"; @@ -157,6 +159,7 @@ class MessageLookup extends MessageLookupByLibrary { "(Optional) Group name": MessageLookupByLibrary.simpleMessage("(Optional) Group name"), "About": MessageLookupByLibrary.simpleMessage("About"), + "Accept": MessageLookupByLibrary.simpleMessage("Accept"), "Account": MessageLookupByLibrary.simpleMessage("Account"), "Account informations": MessageLookupByLibrary.simpleMessage("Account informations"), @@ -178,6 +181,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Avatar has been changed"), "Ban from chat": MessageLookupByLibrary.simpleMessage("Ban from chat"), "Banned": MessageLookupByLibrary.simpleMessage("Banned"), + "Block Device": MessageLookupByLibrary.simpleMessage("Block Device"), "Cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "Change the homeserver": MessageLookupByLibrary.simpleMessage("Change the homeserver"), @@ -242,6 +246,7 @@ class MessageLookup extends MessageLookupByLibrary { "Emote shortcode": MessageLookupByLibrary.simpleMessage("Emote shortcode"), "Empty chat": MessageLookupByLibrary.simpleMessage("Empty chat"), + "Encryption": MessageLookupByLibrary.simpleMessage("Encryption"), "Encryption algorithm": MessageLookupByLibrary.simpleMessage("Encryption algorithm"), "Encryption is not enabled": @@ -351,6 +356,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Please enter your username"), "Public Rooms": MessageLookupByLibrary.simpleMessage("Public Rooms"), "Recording": MessageLookupByLibrary.simpleMessage("Recording"), + "Reject": MessageLookupByLibrary.simpleMessage("Reject"), "Rejoin": MessageLookupByLibrary.simpleMessage("Rejoin"), "Remove": MessageLookupByLibrary.simpleMessage("Remove"), "Remove all other devices": @@ -368,6 +374,8 @@ class MessageLookup extends MessageLookupByLibrary { "Request to read older messages"), "Revoke all permissions": MessageLookupByLibrary.simpleMessage("Revoke all permissions"), + "Room has been upgraded": + MessageLookupByLibrary.simpleMessage("Room has been upgraded"), "Saturday": MessageLookupByLibrary.simpleMessage("Saturday"), "Search for a chat": MessageLookupByLibrary.simpleMessage("Search for a chat"), @@ -388,9 +396,11 @@ class MessageLookup extends MessageLookupByLibrary { "Settings": MessageLookupByLibrary.simpleMessage("Settings"), "Share": MessageLookupByLibrary.simpleMessage("Share"), "Sign up": MessageLookupByLibrary.simpleMessage("Sign up"), + "Skip": MessageLookupByLibrary.simpleMessage("Skip"), "Source code": MessageLookupByLibrary.simpleMessage("Source code"), "Start your first chat :-)": MessageLookupByLibrary.simpleMessage("Start your first chat :-)"), + "Submit": MessageLookupByLibrary.simpleMessage("Submit"), "Sunday": MessageLookupByLibrary.simpleMessage("Sunday"), "System": MessageLookupByLibrary.simpleMessage("System"), "Tap to show menu": @@ -398,12 +408,17 @@ class MessageLookup extends MessageLookupByLibrary { "The encryption has been corrupted": MessageLookupByLibrary.simpleMessage( "The encryption has been corrupted"), + "They Don\'t Match": + MessageLookupByLibrary.simpleMessage("They Don\'t Match"), + "They Match": MessageLookupByLibrary.simpleMessage("They Match"), "This room has been archived.": MessageLookupByLibrary.simpleMessage( "This room has been archived."), "Thursday": MessageLookupByLibrary.simpleMessage("Thursday"), "Try to send again": MessageLookupByLibrary.simpleMessage("Try to send again"), "Tuesday": MessageLookupByLibrary.simpleMessage("Tuesday"), + "Unblock Device": + MessageLookupByLibrary.simpleMessage("Unblock Device"), "Unknown device": MessageLookupByLibrary.simpleMessage("Unknown device"), "Unknown encryption algorithm": MessageLookupByLibrary.simpleMessage( @@ -413,6 +428,7 @@ class MessageLookup extends MessageLookupByLibrary { "Use Amoled compatible colors?"), "Username": MessageLookupByLibrary.simpleMessage("Username"), "Verify": MessageLookupByLibrary.simpleMessage("Verify"), + "Verify User": MessageLookupByLibrary.simpleMessage("Verify User"), "Video call": MessageLookupByLibrary.simpleMessage("Video call"), "Visibility of the chat history": MessageLookupByLibrary.simpleMessage( "Visibility of the chat history"), @@ -451,8 +467,17 @@ class MessageLookup extends MessageLookupByLibrary { "acceptedTheInvitation": m0, "activatedEndToEndEncryption": m1, "alias": MessageLookupByLibrary.simpleMessage("alias"), + "askSSSSCache": MessageLookupByLibrary.simpleMessage( + "Please enter your secure store passphrase or recovery key to cache the keys."), + "askSSSSSign": MessageLookupByLibrary.simpleMessage( + "To be able to sign the other person, please enter your secure store passphrase or recovery key."), + "askSSSSVerify": MessageLookupByLibrary.simpleMessage( + "Please enter your secure store passphrase or recovery key to verify your session."), + "askVerificationRequest": m60, "bannedUser": m2, "byDefaultYouWillBeConnectedTo": m3, + "cachedKeys": + MessageLookupByLibrary.simpleMessage("Successfully cached keys!"), "changedTheChatAvatar": m4, "changedTheChatDescriptionTo": m5, "changedTheChatNameTo": m6, @@ -467,9 +492,17 @@ class MessageLookup extends MessageLookupByLibrary { "changedTheProfileAvatar": m15, "changedTheRoomAliases": m16, "changedTheRoomInvitationLink": m17, + "compareEmojiMatch": MessageLookupByLibrary.simpleMessage( + "Compare and make sure the following emoji match those of the other device:"), + "compareNumbersMatch": MessageLookupByLibrary.simpleMessage( + "Compare and make sure the following numbers match those of the other device:"), "couldNotDecryptMessage": m18, "countParticipants": m19, "createdTheChat": m20, + "crossSigningDisabled": + MessageLookupByLibrary.simpleMessage("Cross-Signing is disabled"), + "crossSigningEnabled": + MessageLookupByLibrary.simpleMessage("Cross-Signing is enabled"), "dateAndTimeOfDay": m21, "dateWithYear": m22, "dateWithoutYear": m23, @@ -481,18 +514,36 @@ class MessageLookup extends MessageLookupByLibrary { "You need to pick an emote shortcode and an image!"), "groupWith": m24, "hasWithdrawnTheInvitationFor": m25, + "incorrectPassphraseOrKey": MessageLookupByLibrary.simpleMessage( + "Incorrect passphrase or recovery key"), "inviteContactToGroup": m26, "inviteText": m27, "invitedUser": m28, "is typing...": MessageLookupByLibrary.simpleMessage("is typing..."), + "isDeviceKeyCorrect": MessageLookupByLibrary.simpleMessage( + "Is the following device key correct?"), "joinedTheChat": m29, + "keysCached": MessageLookupByLibrary.simpleMessage("Keys are cached"), + "keysMissing": MessageLookupByLibrary.simpleMessage("Keys are missing"), "kicked": m30, "kickedAndBanned": m31, "lastActiveAgo": m32, "loadCountMoreParticipants": m33, "logInTo": m34, + "newVerificationRequest": + MessageLookupByLibrary.simpleMessage("New verification request!"), + "noCrossSignBootstrap": MessageLookupByLibrary.simpleMessage( + "Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot."), + "noMegolmBootstrap": MessageLookupByLibrary.simpleMessage( + "Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot."), "numberSelected": m35, "ok": MessageLookupByLibrary.simpleMessage("ok"), + "onlineKeyBackupDisabled": MessageLookupByLibrary.simpleMessage( + "Online Key Backup is disabled"), + "onlineKeyBackupEnabled": MessageLookupByLibrary.simpleMessage( + "Online Key Backup is enabled"), + "passphraseOrKey": + MessageLookupByLibrary.simpleMessage("passphrase or recovery key"), "play": m36, "redactedAnEvent": m37, "rejectedTheInvitation": m38, @@ -505,11 +556,15 @@ class MessageLookup extends MessageLookupByLibrary { "sentASticker": m45, "sentAVideo": m46, "sentAnAudio": m47, + "sessionVerified": + MessageLookupByLibrary.simpleMessage("Session is verified"), "sharedTheLocation": m48, "timeOfDay": m49, "title": MessageLookupByLibrary.simpleMessage("FluffyChat"), "unbannedUser": m50, "unknownEvent": m51, + "unknownSessionVerify": MessageLookupByLibrary.simpleMessage( + "Unknown session, please verify"), "unreadChats": m52, "unreadMessages": m53, "unreadMessagesInChats": m54, @@ -517,6 +572,21 @@ class MessageLookup extends MessageLookupByLibrary { "userAndUserAreTyping": m56, "userIsTyping": m57, "userLeftTheChat": m58, - "userSentUnknownEvent": m59 + "userSentUnknownEvent": m59, + "verifiedSession": MessageLookupByLibrary.simpleMessage( + "Successfully verified session!"), + "verifyManual": MessageLookupByLibrary.simpleMessage("Verify Manually"), + "verifyStart": + MessageLookupByLibrary.simpleMessage("Start Verification"), + "verifySuccess": + MessageLookupByLibrary.simpleMessage("You successfully verified!"), + "verifyTitle": + MessageLookupByLibrary.simpleMessage("Verifying other account"), + "waitingPartnerAcceptRequest": MessageLookupByLibrary.simpleMessage( + "Waiting for partner to accept the request..."), + "waitingPartnerEmoji": MessageLookupByLibrary.simpleMessage( + "Waiting for partner to accept the emoji..."), + "waitingPartnerNumbers": MessageLookupByLibrary.simpleMessage( + "Waiting for partner to accept the numbers...") }; } diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 761b64e..43a9924 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -127,7 +127,7 @@ Future migrate(String clientName, Database db, Store store) async { var sess = olm.Session(); sess.unpickle(credentials['userID'], pickle); await db.storeOlmSession( - clientId, identKey, sess.session_id(), pickle); + clientId, identKey, sess.session_id(), pickle, null); sess?.free(); } } diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 7230661..da86e9f 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -16,10 +16,13 @@ extension RoomStatusExtension on Room { if (directChatPresence.presence.currentlyActive == true) { return L10n.of(context).currentlyActive; } - return L10n.of(context).lastActiveAgo( - DateTime.fromMillisecondsSinceEpoch( - directChatPresence.presence.lastActiveAgo) - .localizedTimeShort(context)); + if (directChatPresence.presence.lastActiveAgo == null) { + return L10n.of(context).lastSeenLongTimeAgo; + } + final time = DateTime.fromMillisecondsSinceEpoch( + DateTime.now().millisecondsSinceEpoch - + directChatPresence.presence.lastActiveAgo); + return L10n.of(context).lastActiveAgo(time.localizedTimeShort(context)); } return L10n.of(context).lastSeenLongTimeAgo; } diff --git a/lib/views/chat_encryption_settings.dart b/lib/views/chat_encryption_settings.dart index 366d4ec..1f44f00 100644 --- a/lib/views/chat_encryption_settings.dart +++ b/lib/views/chat_encryption_settings.dart @@ -1,4 +1,5 @@ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/matrix.dart'; @@ -6,6 +7,9 @@ import 'package:fluffychat/utils/beautify_string_extension.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/views/chat_list.dart'; import 'package:flutter/material.dart'; +import 'key_verification.dart'; +import '../utils/app_route.dart'; +import '../components/dialogs/simple_dialogs.dart'; class ChatEncryptionSettingsView extends StatelessWidget { final String id; @@ -33,6 +37,70 @@ class ChatEncryptionSettings extends StatefulWidget { } class _ChatEncryptionSettingsState extends State { + Future onSelected( + BuildContext context, String action, DeviceKeys key) async { + final room = Matrix.of(context).client.getRoomById(widget.id); + final unblock = () async { + if (key.blocked) { + await key.setBlocked(false); + } + }; + switch (action) { + case 'verify': + await unblock(); + final req = key.startVerification(); + req.onUpdate = () { + if (req.state == KeyVerificationState.done) { + setState(() => null); + } + }; + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: req), + ), + ); + break; + case 'verify_manual': + if (await SimpleDialogs(context).askConfirmation( + titleText: L10n.of(context).isDeviceKeyCorrect, + contentText: key.ed25519Key.beautified, + )) { + await unblock(); + await key.setVerified(true); + setState(() => null); + } + break; + case 'verify_user': + await unblock(); + final req = + await room.client.userDeviceKeys[key.userId].startVerification(); + req.onUpdate = () { + if (req.state == KeyVerificationState.done) { + setState(() => null); + } + }; + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: req), + ), + ); + break; + case 'block': + if (key.directVerified) { + await key.setVerified(false); + } + await key.setBlocked(true); + setState(() => null); + break; + case 'unblock': + await unblock(); + setState(() => null); + break; + } + } + @override Widget build(BuildContext context) { final room = Matrix.of(context).client.getRoomById(widget.id); @@ -68,59 +136,99 @@ class _ChatEncryptionSettingsState extends State { if (i == 0 || deviceKeys[i].userId != deviceKeys[i - 1].userId) Material( - child: ListTile( - leading: Avatar( - room + child: PopupMenuButton( + onSelected: (action) => + onSelected(context, action, deviceKeys[i]), + itemBuilder: (c) { + var items = >[]; + if (room + .client + .userDeviceKeys[deviceKeys[i].userId] + .verified == + UserVerifiedStatus.unknown && + deviceKeys[i].userId != room.client.userID) { + items.add(PopupMenuItem( + child: Text(L10n.of(context).verifyUser), + value: 'verify_user', + )); + } + return items; + }, + child: ListTile( + leading: Avatar( + room + .getUserByMXIDSync(deviceKeys[i].userId) + .avatarUrl, + room + .getUserByMXIDSync(deviceKeys[i].userId) + .calcDisplayname(), + ), + title: Text(room .getUserByMXIDSync(deviceKeys[i].userId) - .avatarUrl, - room - .getUserByMXIDSync(deviceKeys[i].userId) - .calcDisplayname(), + .calcDisplayname()), + subtitle: Text(deviceKeys[i].userId), ), - title: Text(room - .getUserByMXIDSync(deviceKeys[i].userId) - .calcDisplayname()), - subtitle: Text(deviceKeys[i].userId), ), elevation: 2, ), - CheckboxListTile( - title: Text( - "${deviceKeys[i].unsigned["device_display_name"] ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}", - style: TextStyle( - color: deviceKeys[i].blocked - ? Colors.red - : deviceKeys[i].verified - ? Colors.green - : Colors.orange), - ), - subtitle: Text( - deviceKeys[i] - .keys['ed25519:${deviceKeys[i].deviceId}'] - .beautified, - style: TextStyle( - color: - Theme.of(context).textTheme.bodyText2.color), - ), - value: deviceKeys[i].verified, - onChanged: (bool newVal) { - if (newVal == true) { - if (deviceKeys[i].blocked) { - deviceKeys[i] - .setBlocked(false, Matrix.of(context).client); + PopupMenuButton( + onSelected: (action) => + onSelected(context, action, deviceKeys[i]), + itemBuilder: (c) { + var items = >[]; + if (deviceKeys[i].blocked || + !deviceKeys[i].verified) { + if (deviceKeys[i].userId == room.client.userID) { + items.add(PopupMenuItem( + child: Text(L10n.of(context).verifyStart), + value: 'verify', + )); + items.add(PopupMenuItem( + child: Text(L10n.of(context).verifyManual), + value: 'verify_manual', + )); + } else { + items.add(PopupMenuItem( + child: Text(L10n.of(context).verifyUser), + value: 'verify_user', + )); } - deviceKeys[i] - .setVerified(true, Matrix.of(context).client); - } else { - if (deviceKeys[i].verified) { - deviceKeys[i].setVerified( - false, Matrix.of(context).client); - } - deviceKeys[i] - .setBlocked(true, Matrix.of(context).client); } - setState(() => null); + if (deviceKeys[i].blocked) { + items.add(PopupMenuItem( + child: Text(L10n.of(context).unblockDevice), + value: 'unblock', + )); + } + if (!deviceKeys[i].blocked) { + items.add(PopupMenuItem( + child: Text(L10n.of(context).blockDevice), + value: 'block', + )); + } + return items; }, + child: ListTile( + title: Text( + "${deviceKeys[i].unsigned["device_display_name"] ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}", + style: TextStyle( + color: deviceKeys[i].blocked + ? Colors.red + : deviceKeys[i].verified + ? Colors.green + : Colors.orange), + ), + subtitle: Text( + deviceKeys[i] + .keys['ed25519:${deviceKeys[i].deviceId}'] + .beautified, + style: TextStyle( + color: Theme.of(context) + .textTheme + .bodyText2 + .color), + ), + ), ), ], ), diff --git a/lib/views/key_verification.dart b/lib/views/key_verification.dart new file mode 100644 index 0000000..94ca07c --- /dev/null +++ b/lib/views/key_verification.dart @@ -0,0 +1,347 @@ +import 'package:flutter/material.dart'; +import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'chat_list.dart'; +import '../components/adaptive_page_layout.dart'; +import '../components/avatar.dart'; +import '../components/dialogs/simple_dialogs.dart'; +import '../l10n/l10n.dart'; + +class KeyVerificationView extends StatelessWidget { + final KeyVerification request; + + KeyVerificationView({this.request}); + + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: KeyVerificationPage(request: request), + ); + } +} + +class KeyVerificationPage extends StatefulWidget { + final KeyVerification request; + + KeyVerificationPage({this.request}); + + @override + _KeyVerificationPageState createState() => _KeyVerificationPageState(); +} + +class _KeyVerificationPageState extends State { + void Function() originalOnUpdate; + + @override + void initState() { + originalOnUpdate = widget.request.onUpdate; + widget.request.onUpdate = () { + if (originalOnUpdate != null) { + originalOnUpdate(); + } + setState(() => null); + }; + widget.request.client.getProfileFromUserId(widget.request.userId).then((p) { + profile = p; + setState(() => null); + }); + super.initState(); + } + + @override + void dispose() { + widget.request.onUpdate = + originalOnUpdate; // don't want to get updates anymore + if (![KeyVerificationState.error, KeyVerificationState.done] + .contains(widget.request.state)) { + widget.request.cancel('m.user'); + } + super.dispose(); + } + + Profile profile; + + @override + Widget build(BuildContext context) { + Widget body; + final buttons = []; + switch (widget.request.state) { + case KeyVerificationState.askSSSS: + // prompt the user for their ssss passphrase / key + final textEditingController = TextEditingController(); + String input; + final checkInput = () async { + if (input == null) { + return; + } + SimpleDialogs(context).showLoadingDialog(context); + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + await widget.request.openSSSS(recoveryKey: input); + valid = true; + } catch (_) { + try { + await widget.request.openSSSS(passphrase: input); + valid = true; + } catch (_) { + valid = false; + } + } + await Navigator.of(context)?.pop(); + if (!valid) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).incorrectPassphraseOrKey, + ); + } + }; + body = Container( + margin: EdgeInsets.only(left: 8.0, right: 8.0), + child: Column( + children: [ + Text(L10n.of(context).askSSSSSign, + style: TextStyle(fontSize: 20)), + Container(height: 10), + TextField( + controller: textEditingController, + autofocus: false, + autocorrect: false, + onSubmitted: (s) { + input = s; + checkInput(); + }, + minLines: 1, + maxLines: 1, + obscureText: true, + decoration: InputDecoration( + hintText: L10n.of(context).passphraseOrKey, + prefixStyle: TextStyle(color: Theme.of(context).primaryColor), + suffixStyle: TextStyle(color: Theme.of(context).primaryColor), + border: OutlineInputBorder(), + ), + ), + ], + mainAxisSize: MainAxisSize.min, + ), + ); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).submit), + onPressed: () { + input = textEditingController.text; + checkInput(); + }, + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text(L10n.of(context).skip), + onPressed: () => widget.request.openSSSS(skip: true), + )); + break; + case KeyVerificationState.askAccept: + body = Container( + child: Text( + L10n.of(context).askVerificationRequest(widget.request.userId), + style: TextStyle(fontSize: 20)), + margin: EdgeInsets.only(left: 8.0, right: 8.0), + ); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).accept), + onPressed: () => widget.request.acceptVerification(), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text(L10n.of(context).reject), + onPressed: () { + widget.request.rejectVerification().then((_) { + Navigator.of(context).pop(); + }); + }, + )); + break; + case KeyVerificationState.waitingAccept: + body = Column( + children: [ + CircularProgressIndicator(), + Container(height: 10), + Text(L10n.of(context).waitingPartnerAcceptRequest), + ], + mainAxisSize: MainAxisSize.min, + ); + break; + case KeyVerificationState.askSas: + var emojiWidgets = []; + // maybe add a button to switch between the two and only determine default + // view for if "emoji" is a present sasType or not? + String compareText; + if (widget.request.sasTypes.contains('emoji')) { + compareText = L10n.of(context).compareEmojiMatch; + emojiWidgets = + widget.request.sasEmojis.map((e) => _Emoji(e)).toList(); + } else { + compareText = L10n.of(context).compareNumbersMatch; + final numbers = widget.request.sasNumbers; + emojiWidgets = [ + Text(numbers[0].toString(), style: TextStyle(fontSize: 40)), + Text('-', style: TextStyle(fontSize: 40)), + Text(numbers[1].toString(), style: TextStyle(fontSize: 40)), + Text('-', style: TextStyle(fontSize: 40)), + Text(numbers[2].toString(), style: TextStyle(fontSize: 40)), + ]; + } + body = Column( + children: [ + Container( + child: Text(compareText, style: TextStyle(fontSize: 20)), + margin: EdgeInsets.only(left: 8.0, right: 8.0), + ), + Container(height: 10), + RichText( + text: TextSpan( + children: + emojiWidgets.map((w) => WidgetSpan(child: w)).toList(), + ), + textAlign: TextAlign.center, + ), + ], + mainAxisSize: MainAxisSize.min, + ); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).theyMatch), + onPressed: () => widget.request.acceptSas(), + )); + buttons.add(RaisedButton( + textColor: Theme.of(context).primaryColor, + elevation: 5, + color: Colors.white, + child: Text(L10n.of(context).theyDontMatch), + onPressed: () => widget.request.rejectSas(), + )); + break; + case KeyVerificationState.waitingSas: + var acceptText = widget.request.sasTypes.contains('emoji') + ? L10n.of(context).waitingPartnerEmoji + : L10n.of(context).waitingPartnerNumbers; + body = Column( + children: [ + CircularProgressIndicator(), + Container(height: 10), + Text(acceptText), + ], + mainAxisSize: MainAxisSize.min, + ); + break; + case KeyVerificationState.done: + body = Column( + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 200.0), + Container(height: 10), + Text(L10n.of(context).verifySuccess), + ], + mainAxisSize: MainAxisSize.min, + ); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).close), + onPressed: () => Navigator.of(context).pop(), + )); + break; + case KeyVerificationState.error: + body = Column( + children: [ + Icon(Icons.cancel, color: Colors.red, size: 200.0), + Container(height: 10), + Text( + 'Error ${widget.request.canceledCode}: ${widget.request.canceledReason}'), + ], + mainAxisSize: MainAxisSize.min, + ); + buttons.add(RaisedButton( + color: Theme.of(context).primaryColor, + elevation: 5, + textColor: Colors.white, + child: Text(L10n.of(context).close), + onPressed: () => Navigator.of(context).pop(), + )); + break; + } + body ??= Text('ERROR: Unknown state ' + widget.request.state.toString()); + final otherName = profile?.displayname ?? widget.request.userId; + var bottom; + if (widget.request.deviceId != null) { + final deviceName = widget + .request + .client + .userDeviceKeys[widget.request.userId] + ?.deviceKeys[widget.request.deviceId] + ?.deviceDisplayName ?? + ''; + bottom = PreferredSize( + child: Text('$deviceName (${widget.request.deviceId})', + style: TextStyle(color: Theme.of(context).textTheme.caption.color)), + preferredSize: Size(0.0, 20.0), + ); + } + return Scaffold( + appBar: AppBar( + title: ListTile( + leading: Avatar(profile?.avatarUrl, otherName), + contentPadding: EdgeInsets.zero, + title: Text(L10n.of(context).verifyTitle), + isThreeLine: otherName != widget.request.userId, + subtitle: Column( + children: [ + Text(otherName), + if (otherName != widget.request.userId) + Text(widget.request.userId), + ], + crossAxisAlignment: CrossAxisAlignment.start, + ), + ), + elevation: 0, + bottom: bottom, + ), + extendBody: true, + extendBodyBehindAppBar: true, + body: Center( + child: body, + ), + persistentFooterButtons: buttons.isEmpty ? null : buttons, + ); + } +} + +class _Emoji extends StatelessWidget { + final KeyVerificationEmoji emoji; + + _Emoji(this.emoji); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(emoji.emoji, style: TextStyle(fontSize: 50)), + Text(emoji.name), + Container(height: 10, width: 5), + ], + ); + } +} diff --git a/lib/views/settings.dart b/lib/views/settings.dart index 4a043dc..20c06a1 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -38,6 +38,10 @@ class Settings extends StatefulWidget { class _SettingsState extends State { Future profileFuture; dynamic profile; + Future crossSigningCachedFuture; + bool crossSigningCached; + Future megolmBackupCachedFuture; + bool megolmBackupCached; void logoutAction(BuildContext context) async { if (await SimpleDialogs(context).askConfirmation() == false) { @@ -123,12 +127,65 @@ class _SettingsState extends State { setState(() => null); } + Future requestSSSSCache(BuildContext context) async { + final handle = Matrix.of(context).client.encryption.ssss.open(); + final str = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).askSSSSCache, + hintText: L10n.of(context).passphraseOrKey, + password: true, + ); + if (str != null) { + SimpleDialogs(context).showLoadingDialog(context); + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + handle.unlock(recoveryKey: str); + valid = true; + } catch (_) { + try { + handle.unlock(passphrase: str); + valid = true; + } catch (_) { + valid = false; + } + } + await Navigator.of(context)?.pop(); + if (valid) { + await handle.maybeCacheAll(); + await SimpleDialogs(context).inform( + contentText: L10n.of(context).cachedKeys, + ); + setState(() { + crossSigningCachedFuture = null; + crossSigningCached = null; + megolmBackupCachedFuture = null; + megolmBackupCached = null; + }); + } else { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).incorrectPassphraseOrKey, + ); + } + } + } + @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - profileFuture ??= client.ownProfile; - profileFuture.then((p) { + profileFuture ??= client.ownProfile.then((p) { if (mounted) setState(() => profile = p); + return p; + }); + crossSigningCachedFuture ??= + client.encryption.crossSigning.isCached().then((c) { + if (mounted) setState(() => crossSigningCached = c); + return c; + }); + megolmBackupCachedFuture ??= + client.encryption.keyManager.isCached().then((c) { + if (mounted) setState(() => megolmBackupCached = c); + return c; }); return Scaffold( body: NestedScrollView( @@ -286,6 +343,110 @@ class _SettingsState extends State { onTap: () => logoutAction(context), ), Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).encryption, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + trailing: Icon(Icons.compare_arrows), + title: Text(client.encryption.crossSigning.enabled + ? L10n.of(context).crossSigningEnabled + : L10n.of(context).crossSigningDisabled), + subtitle: client.encryption.crossSigning.enabled + ? Text(client.isUnknownSession + ? L10n.of(context).unknownSessionVerify + : L10n.of(context).sessionVerified + + ', ' + + (crossSigningCached == null + ? '⌛' + : (crossSigningCached + ? L10n.of(context).keysCached + : L10n.of(context).keysMissing))) + : null, + onTap: () async { + if (!client.encryption.crossSigning.enabled) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).noCrossSignBootstrap, + ); + return; + } + if (client.isUnknownSession) { + final str = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).askSSSSVerify, + hintText: L10n.of(context).passphraseOrKey, + password: true, + ); + if (str != null) { + SimpleDialogs(context).showLoadingDialog(context); + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + await client.encryption.crossSigning + .selfSign(recoveryKey: str); + valid = true; + } catch (_) { + try { + await client.encryption.crossSigning + .selfSign(passphrase: str); + valid = true; + } catch (_) { + valid = false; + } + } + await Navigator.of(context)?.pop(); + if (valid) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).verifiedSession, + ); + setState(() { + crossSigningCachedFuture = null; + crossSigningCached = null; + megolmBackupCachedFuture = null; + megolmBackupCached = null; + }); + } else { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).incorrectPassphraseOrKey, + ); + } + } + } + if (!(await client.encryption.crossSigning.isCached())) { + await requestSSSSCache(context); + } + }, + ), + ListTile( + trailing: Icon(Icons.wb_cloudy), + title: Text(client.encryption.keyManager.enabled + ? L10n.of(context).onlineKeyBackupEnabled + : L10n.of(context).onlineKeyBackupDisabled), + subtitle: client.encryption.keyManager.enabled + ? Text(megolmBackupCached == null + ? '⌛' + : (megolmBackupCached + ? L10n.of(context).keysCached + : L10n.of(context).keysMissing)) + : null, + onTap: () async { + if (!client.encryption.keyManager.enabled) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).noMegolmBootstrap, + ); + return; + } + if (!(await client.encryption.keyManager.isCached())) { + await requestSSSSCache(context); + } + }, + ), + Divider(thickness: 1), ListTile( title: Text( L10n.of(context).about, diff --git a/pubspec.lock b/pubspec.lock index f168bda..d1ad4e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" async: dependency: transitive description: @@ -36,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.1" + base58check: + dependency: transitive + description: + name: base58check + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" boolean_selector: dependency: transitive description: @@ -127,6 +141,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" encrypted_moor: dependency: "direct main" description: @@ -136,19 +157,12 @@ packages: url: "https://github.com/simolus3/moor.git" source: git version: "1.0.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" famedlysdk: dependency: "direct main" description: path: "." - ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f - resolved-ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f + ref: "8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06" + resolved-ref: "8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -487,10 +501,10 @@ packages: description: path: "." ref: "1.x.y" - resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5 + resolved-ref: "8e4fcccff7a2d4d0bd5142964db092bf45061905" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git - version: "1.1.1" + version: "1.2.0" open_file: dependency: "direct main" description: @@ -512,13 +526,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.10" + password_hash: + dependency: transitive + description: + name: password_hash + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.6.4" path_drawing: dependency: transitive description: @@ -596,6 +617,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.2" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" random_string: dependency: "direct main" description: @@ -733,21 +761,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.14.7" + version: "1.14.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.16" + version: "0.2.15" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.7" + version: "0.3.4" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 085953e..4e41a37 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f + ref: 8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06 localstorage: ^3.0.1+4 bubble: ^1.1.9+1