From d89512e49556e16ec00b9ebabe2540ccba515539 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 | 43 ++++++++++++++++++++++--- lib/components/matrix.dart | 13 ++++++++ lib/components/user_bottom_sheet.dart | 30 ++++++++++++++++- lib/l10n/intl_en.arb | 2 +- lib/views/chat_encryption_settings.dart | 15 ++++----- pubspec.lock | 6 ++-- pubspec.yaml | 2 +- 9 files changed, 120 insertions(+), 29 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..176e7c1b 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; @@ -137,12 +140,44 @@ 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: () async { + 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( + displayEvent.requestKey()); + } + }, ), 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..72c65275 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; @@ -74,7 +76,9 @@ class UserBottomSheet extends StatelessWidget { @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 +149,30 @@ 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: () async { + if (verificationStatus != UserVerifiedStatus.unknown) { + return; + } + final req = await client.userDeviceKeys[user.id] + .startVerification(); + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + KeyVerificationView(request: req), + ), + ); + }, + ), if (user.id != Matrix.of(context).client.userID) PopupMenuButton( itemBuilder: (_) => items, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 73462e1d..f28f0879 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1743,4 +1743,4 @@ "type": "text", "placeholders": {} } -} \ No newline at end of file +} 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 b7474137..e457676a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -215,8 +215,8 @@ packages: 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" @@ -1144,5 +1144,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.2 <2.11.0" + 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 98d6aaf8..0449a455 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,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