diff --git a/lib/src/model/json/onboarding_state.model.dart b/lib/src/model/json/onboarding_state.model.dart index cdd4eac4..a9fc7909 100644 --- a/lib/src/model/json/onboarding_state.model.dart +++ b/lib/src/model/json/onboarding_state.model.dart @@ -14,6 +14,7 @@ class ReceivedRecoveryShare { required this.myAvatarSvg, required this.threshold, required this.sharedSecretDataBytes, + this.trustedFriendAvatarSvg, }); factory ReceivedRecoveryShare.fromJson(Map json) => @@ -22,6 +23,7 @@ class ReceivedRecoveryShare { final int messageId; final String trustedFriendDisplayName; + final List? trustedFriendAvatarSvg; final String myDisplayName; final int myUserId; @@ -58,7 +60,6 @@ class OnboardingState { List? encryptionKey; bool emailRecoveryRequested; - List receivedShares = []; Map toJson() => _$OnboardingStateToJson(this); diff --git a/lib/src/model/json/onboarding_state.model.g.dart b/lib/src/model/json/onboarding_state.model.g.dart index 67ba5930..7987797a 100644 --- a/lib/src/model/json/onboarding_state.model.g.dart +++ b/lib/src/model/json/onboarding_state.model.g.dart @@ -11,6 +11,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson( ) => ReceivedRecoveryShare( messageId: (json['messageId'] as num).toInt(), trustedFriendDisplayName: json['trustedFriendDisplayName'] as String, + trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), myDisplayName: json['myDisplayName'] as String, myUserId: (json['myUserId'] as num).toInt(), myAvatarSvg: (json['myAvatarSvg'] as List?) @@ -27,6 +30,7 @@ Map _$ReceivedRecoveryShareToJson( ) => { 'messageId': instance.messageId, 'trustedFriendDisplayName': instance.trustedFriendDisplayName, + 'trustedFriendAvatarSvg': instance.trustedFriendAvatarSvg, 'myDisplayName': instance.myDisplayName, 'myUserId': instance.myUserId, 'myAvatarSvg': instance.myAvatarSvg, diff --git a/lib/src/services/backup.service.dart b/lib/src/services/backup.service.dart index 42fb4e67..242166b2 100644 --- a/lib/src/services/backup.service.dart +++ b/lib/src/services/backup.service.dart @@ -247,7 +247,7 @@ class BackupService { } if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) { - // The KeyManager was restored sucessfully, restore the archive now. + // The KeyManager was restored successfully, restore the archive now. try { final downloadToken = await RustBackupArchive.getBackupDownloadToken(); diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index ced9abd4..ecca9060 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -187,36 +187,29 @@ class PasswordlessRecoveryService { switch (secondFactorType) { case SecondFactorType.email: - - // The serverKey is encrypted with the email; this protects the server from seeing the user's email address, while - // also ensuring that the server can only send the real secret to the user's configured email, as a different - // email will result in a different secret key. - // This is only stored so the user can see there email, and verify that he has set the valid mail... config.email = secondFactorValue; emailHint = createEmailHint(secondFactorValue); // E-Mail Protection: - // - Server can only learn the email during recovery. Ensured as the server gets the NONCE and MAC to decrpyt during recovery. - // - Trusted-friends: Server key is send to the mail, they whould need access to the user's mail account. + // - Server can only learn the email during recovery. Ensured as the server gets the NONCE to decrypt only during recovery. + // - Trusted-friends: Server key is only sent to the mail, they would need access to the user's mail account. secondFactorEncryptedServerKeyKey = SecretKey( Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes), ); case SecondFactorType.pin: - // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin. + // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin config.pinSeed = getRandomUint8List(32); - // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after X tries, the - // unlock token is required to prevent a malicous user (except the trusted friends) to triger this deletion. + // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the + // unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion. config.pinUnlockToken = getRandomUint8List(32); // Brute-force protection for the user's pin: // - Server: Does not know the seed. - // - Trusted friends: - // Can only check the result X times before the server deletes the key. As they do not have - // the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10 - // tries. + // - Trusted friends: Can only check the result 10 times before the server deletes the key. As they do not have + // the mac and the cipher text they are unable to brute-force the pin locally. And the server only allows 10 tries. final pinProtectionKey = await Hmac.sha256().calculateMac( Uint8List.fromList(utf8.encode(secondFactorValue)), secretKey: SecretKey(config.pinSeed!), @@ -224,7 +217,7 @@ class PasswordlessRecoveryService { // To restore the user has to provide the server with this encryption key. The server then can verify the // correct pin was entered, when decrypting the server key as he also receives the mac and nonce from the user - // during recovery. Only when the mac is correct the server provides the user with the serverKey. + // Only when the mac is correct the server provides the user with the serverKey. secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes); case SecondFactorType.none: @@ -232,7 +225,7 @@ class PasswordlessRecoveryService { if (secondFactorEncryptedServerKeyKey != null) { // The server key is used to encrypt the RecoveryData of the users. This ensures that when the trusted friends - // colaberate, they additional need the serverKey to decrypt the user's key. + // collaborate, they additionally need the serverKey to decrypt the user's key. serverKey = getRandomUint8List(32); final secretBox = await xchacha20.encrypt( @@ -243,7 +236,7 @@ class PasswordlessRecoveryService { // The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit // because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This - // ensures that the server never learns the users orginal pin, as he is missing the pin_seed and also unable to + // ensures that the server never learns the user's original pin, as he is missing the pin_seed and also unable to // brute-force the email of the user as he does not have the nonce. encryptedServerKey = Uint8List.fromList([ ...secretBox.cipherText, @@ -323,7 +316,7 @@ class PasswordlessRecoveryService { unawaited(performHeartbeat()); - // The passwordless is configured sucessfully. + // The passwordless is configured successfully. return true; } @@ -396,7 +389,7 @@ class PasswordlessRecoveryService { clock.now().difference(lastContactHeartbeat).inHours >= 24; if (isContactHeartbeatOlderThan24h) { - // Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send. + // Get all contacts where recoveryLastHeartbeat is NULL. Then for each contact send. // recoveryLastHeartbeat is ONLY updated in case the contact has responded. final pendingShares = await (twonlyDB.select(twonlyDB.contacts)..where( @@ -542,7 +535,7 @@ class PasswordlessRecoveryService { final recoveryLastHeartbeat = const ListEquality().equals(computedHash, msg.hash) ? clock.now() - : null; // The stored share not valid (maybe a old backup was restored). This will cause the performHeartbeat to resend him his share + : null; // The stored share not valid (maybe an old backup was restored). This will cause the performHeartbeat to resend him his share Log.info( '[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)', ); @@ -602,6 +595,9 @@ class PasswordlessRecoveryService { final receivedShare = ReceivedRecoveryShare( messageId: msgId, trustedFriendDisplayName: share.trustedFriend.displayName, + trustedFriendAvatarSvg: share.trustedFriend.hasAvatar() + ? share.trustedFriend.avatar + : null, myDisplayName: share.shareUser.displayName, myUserId: share.shareUser.userId.toInt(), myAvatarSvg: share.shareUser.hasAvatar() diff --git a/lib/src/visual/elements/contact_chip.element.dart b/lib/src/visual/elements/contact_chip.element.dart index f74356c1..e4974c02 100644 --- a/lib/src/visual/elements/contact_chip.element.dart +++ b/lib/src/visual/elements/contact_chip.element.dart @@ -1,7 +1,10 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; @@ -12,6 +15,7 @@ class ContactChip extends StatelessWidget { const ContactChip({ this.contact, this.username, + this.avatarSvg, this.onTap, super.key, }) : assert( @@ -21,6 +25,7 @@ class ContactChip extends StatelessWidget { final Contact? contact; final String? username; + final List? avatarSvg; final void Function(int)? onTap; @override @@ -43,6 +48,9 @@ class ContactChip extends StatelessWidget { avatar: AvatarIcon( contactId: contact?.userId, fontSize: 10, + svg: avatarSvg != null + ? getAvatarSvg(Uint8List.fromList(avatarSvg!)) + : null, ), label: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 50916694..5cc36dda 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -364,7 +364,7 @@ class _MediaViewerViewState extends State { return advanceToNextMediaOrExit(); } - // The server can now delete the encrypted bytes, as the users has sucessfully opened it. + // The server can now delete the encrypted bytes, as the user has successfully opened it. Log.info( 'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}', ); diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index d1bb2d6c..045d54b6 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -543,6 +543,7 @@ class _RecoverPasswordlessState extends State { children: shares.map((share) { return ContactChip( username: share.trustedFriendDisplayName, + avatarSvg: share.trustedFriendAvatarSvg, ); }).toList(), ),