From 1ff986e7be7f7fedc3b6b2edaf4b62c4a0f51986 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 31 Oct 2020 10:39:38 +0100 Subject: [PATCH] feat: Better encryption / verification --- CHANGELOG.md | 8 +++++ lib/components/encryption_button.dart | 30 ++++++++++------ lib/components/list_items/message.dart | 42 +++++++++++++++++++--- lib/components/matrix.dart | 13 +++++++ lib/components/user_bottom_sheet.dart | 32 ++++++++++++++++- lib/views/chat_encryption_settings.dart | 15 ++++---- pubspec.lock | 46 ++++++++++++------------- pubspec.yaml | 2 +- 8 files changed, 140 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7fac2e4..6a42f4ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Version 0.22.0 +### Features +- Broadcast self-verification +### Changes +- Undecryptable events have a "verify" button, if you haven't verified yet +- User bottom sheet lists verified status +- Lock icon next to input bar can now be red + # Version 0.21.0 - 2020-10-28 ### Features - New user viewer diff --git a/lib/components/encryption_button.dart b/lib/components/encryption_button.dart index acd7cb9d..08973115 100644 --- a/lib/components/encryption_button.dart +++ b/lib/components/encryption_button.dart @@ -62,20 +62,30 @@ class _EncryptionButtonState extends State { .onSync .stream .listen((s) => setState(() => null)); - return FutureBuilder>( - future: widget.room.encrypted ? widget.room.getUserDeviceKeys() : null, + return FutureBuilder>( + future: + widget.room.encrypted ? widget.room.requestParticipants() : null, builder: (BuildContext context, snapshot) { Color color; if (widget.room.encrypted && snapshot.hasData) { - var data = snapshot.data; - final deviceKeysList = data; - color = Colors.orange; - if (deviceKeysList.indexWhere((DeviceKeys deviceKeys) => - deviceKeys.verified == false && - deviceKeys.blocked == false) == - -1) { - color = Colors.black.withGreen(220).withOpacity(0.75); + final users = snapshot.data; + users.removeWhere((u) => + !{Membership.invite, Membership.join}.contains(u.membership) || + !widget.room.client.userDeviceKeys.containsKey(u.id)); + var allUsersValid = true; + var oneUserInvalid = false; + for (final u in users) { + final status = widget.room.client.userDeviceKeys[u.id].verified; + if (status != UserVerifiedStatus.verified) { + allUsersValid = false; + } + if (status == UserVerifiedStatus.unknownDevice) { + oneUserInvalid = true; + } } + color = oneUserInvalid + ? Colors.red + : (allUsersValid ? Colors.green : Colors.orange); } else if (!widget.room.encrypted && widget.room.joinRules != JoinRules.public) { color = null; diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 3694b3d7..145e46de 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -1,4 +1,5 @@ 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'; @@ -13,6 +14,8 @@ import '../avatar.dart'; import '../matrix.dart'; import '../message_reactions.dart'; import 'state_message.dart'; +import '../../views/key_verification.dart'; +import '../../utils/app_route.dart'; class Message extends StatelessWidget { final Event event; @@ -37,6 +40,36 @@ class Message extends StatelessWidget { /// of touchscreen. static bool useMouse = false; + void _verifyOrRequestKey(BuildContext context) async { + final client = Matrix.of(context).client; + if (client.isUnknownSession && client.encryption.crossSigning.enabled) { + final req = + await client.userDeviceKeys[client.userID].startVerification(); + req.onUpdate = () async { + if (req.state == KeyVerificationState.done) { + for (var i = 0; i < 12; i++) { + if (await client.encryption.keyManager.isCached()) { + break; + } + await Future.delayed(Duration(seconds: 1)); + } + final timeline = await event.room.getTimeline(); + timeline.requestKeys(); + timeline.cancelSubscriptions(); + } + }; + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: req), + ), + ); + } else { + await SimpleDialogs(context).tryRequestWithLoadingDialog( + event.getDisplayEvent(timeline).requestKey()); + } + } + @override Widget build(BuildContext context) { if (event.type == EventTypes.Unknown) { @@ -137,12 +170,13 @@ class Message extends StatelessWidget { RaisedButton( color: color.withAlpha(100), child: Text( - L10n.of(context).requestPermission, + client.isUnknownSession && + client.encryption.crossSigning.enabled + ? L10n.of(context).verify + : L10n.of(context).requestPermission, style: TextStyle(color: textColor), ), - onPressed: () => SimpleDialogs(context) - .tryRequestWithLoadingDialog( - displayEvent.requestKey()), + onPressed: () => _verifyOrRequestKey(context), ), SizedBox(height: 4), Opacity( diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 6e57617d..4f359959 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -269,10 +269,21 @@ class MatrixState extends State { }); onKeyVerificationRequestSub ??= client.onKeyVerificationRequest.stream .listen((KeyVerification request) async { + var hidPopup = false; + request.onUpdate = () { + if (!hidPopup && + {KeyVerificationState.done, KeyVerificationState.error} + .contains(request.state)) { + Navigator.of(context, rootNavigator: true).pop('dialog'); + } + hidPopup = true; + }; if (await SimpleDialogs(context).askConfirmation( titleText: L10n.of(context).newVerificationRequest, contentText: L10n.of(context).askVerificationRequest(request.userId), )) { + request.onUpdate = null; + hidPopup = true; await request.acceptVerification(); await Navigator.of(context).push( AppRoute.defaultRoute( @@ -281,6 +292,8 @@ class MatrixState extends State { ), ); } else { + request.onUpdate = null; + hidPopup = true; await request.rejectVerification(); } }); diff --git a/lib/components/user_bottom_sheet.dart b/lib/components/user_bottom_sheet.dart index 4757f3f4..0d1c91d0 100644 --- a/lib/components/user_bottom_sheet.dart +++ b/lib/components/user_bottom_sheet.dart @@ -12,6 +12,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../utils/presence_extension.dart'; import 'dialogs/simple_dialogs.dart'; import 'matrix.dart'; +import '../views/key_verification.dart'; +import '../utils/app_route.dart'; class UserBottomSheet extends StatelessWidget { final User user; @@ -72,9 +74,22 @@ class UserBottomSheet extends StatelessWidget { } } + void _verifyAction(BuildContext context) async { + final client = Matrix.of(context).client; + final req = await client.userDeviceKeys[user.id].startVerification(); + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: req), + ), + ); + } + @override Widget build(BuildContext context) { - final presence = Matrix.of(context).client.presences[user.id]; + final client = Matrix.of(context).client; + final presence = client.presences[user.id]; + final verificationStatus = client.userDeviceKeys[user.id]?.verified; var items = >[]; if (onMention != null) { @@ -145,6 +160,21 @@ class UserBottomSheet extends StatelessWidget { ), title: Text(user.calcDisplayname()), actions: [ + if (verificationStatus != null) + InkWell( + child: Icon( + Icons.lock, + color: { + UserVerifiedStatus.unknownDevice: Colors.red, + UserVerifiedStatus.verified: Colors.green, + }[verificationStatus] ?? + Colors.orange, + ), + onTap: () => + verificationStatus == UserVerifiedStatus.unknown + ? _verifyAction(context) + : null, + ), if (user.id != Matrix.of(context).client.userID) PopupMenuButton( itemBuilder: (_) => items, diff --git a/lib/views/chat_encryption_settings.dart b/lib/views/chat_encryption_settings.dart index 84de318a..6f014b27 100644 --- a/lib/views/chat_encryption_settings.dart +++ b/lib/views/chat_encryption_settings.dart @@ -143,11 +143,10 @@ class _ChatEncryptionSettingsState extends State { itemBuilder: (c) { var items = >[]; if (room - .client - .userDeviceKeys[deviceKeys[i].userId] - .verified == - UserVerifiedStatus.unknown && - deviceKeys[i].userId != room.client.userID) { + .client + .userDeviceKeys[deviceKeys[i].userId] + .verified == + UserVerifiedStatus.unknown) { items.add(PopupMenuItem( child: Text(L10n.of(context).verifyUser), value: 'verify_user', @@ -211,7 +210,7 @@ class _ChatEncryptionSettingsState extends State { }, child: ListTile( title: Text( - "${deviceKeys[i].unsigned["device_display_name"] ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}", + '${deviceKeys[i].deviceDisplayName ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}', style: TextStyle( color: deviceKeys[i].blocked ? Colors.red @@ -220,9 +219,7 @@ class _ChatEncryptionSettingsState extends State { : Colors.orange), ), subtitle: Text( - deviceKeys[i] - .keys['ed25519:${deviceKeys[i].deviceId}'] - .beautified, + deviceKeys[i].ed25519Key.beautified, style: TextStyle( color: Theme.of(context) .textTheme diff --git a/pubspec.lock b/pubspec.lock index 1f72c0f9..59f4c789 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.2" + version: "2.5.0-nullsafety.1" base58check: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.2" + version: "2.1.0-nullsafety.1" bot_toast: dependency: "direct main" description: @@ -98,14 +98,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.4" + version: "1.1.0-nullsafety.3" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.2" + version: "1.2.0-nullsafety.1" circular_check_box: dependency: "direct main" description: @@ -126,14 +126,14 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.2" + version: "1.1.0-nullsafety.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.4" + version: "1.15.0-nullsafety.3" convert: dependency: transitive description: @@ -203,13 +203,13 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.2" + version: "1.2.0-nullsafety.1" famedlysdk: dependency: "direct main" description: path: "." - ref: "955fb747c29eab76b17eb9a13ebc15026e917fb8" - resolved-ref: "955fb747c29eab76b17eb9a13ebc15026e917fb8" + ref: "15d817023d34f813e95eba6ca8c71c575b8c2457" + resolved-ref: "15d817023d34f813e95eba6ca8c71c575b8c2457" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -540,7 +540,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.2" + version: "0.12.10-nullsafety.1" matrix_file_e2ee: dependency: transitive description: @@ -561,7 +561,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.5" + version: "1.3.0-nullsafety.3" mime: dependency: transitive description: @@ -661,7 +661,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0-nullsafety.1" path_provider: dependency: "direct main" description: @@ -876,7 +876,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.3" + version: "1.8.0-nullsafety.2" sqflite: dependency: "direct main" description: @@ -911,21 +911,21 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.5" + version: "1.10.0-nullsafety.1" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.2" + version: "2.1.0-nullsafety.1" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.2" + version: "1.1.0-nullsafety.1" swipe_to_action: dependency: "direct main" description: @@ -946,28 +946,28 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.2" + version: "1.2.0-nullsafety.1" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.0-nullsafety.7" + version: "1.16.0-nullsafety.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.4" + version: "0.2.19-nullsafety.2" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.12-nullsafety.7" + version: "0.3.12-nullsafety.5" timezone: dependency: transitive description: @@ -981,7 +981,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.4" + version: "1.3.0-nullsafety.3" universal_html: dependency: "direct main" description: @@ -1065,7 +1065,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.4" + version: "2.1.0-nullsafety.3" vm_service: dependency: transitive description: @@ -1137,5 +1137,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.11.0-0.0 <=2.11.0-260.0.dev" + dart: ">=2.10.2 <=2.11.0-161.0.dev" flutter: ">=1.22.2 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1ac491cf..d14e03d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 955fb747c29eab76b17eb9a13ebc15026e917fb8 + ref: 15d817023d34f813e95eba6ca8c71c575b8c2457 localstorage: ^3.0.3+6 file_picker_cross: 4.2.2