fix comments and show shared contacts avatar
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled

This commit is contained in:
otsmr 2026-07-16 10:31:58 +02:00
parent 1b37268577
commit df9b0d66bd
7 changed files with 33 additions and 23 deletions

View file

@ -14,6 +14,7 @@ class ReceivedRecoveryShare {
required this.myAvatarSvg,
required this.threshold,
required this.sharedSecretDataBytes,
this.trustedFriendAvatarSvg,
});
factory ReceivedRecoveryShare.fromJson(Map<String, dynamic> json) =>
@ -22,6 +23,7 @@ class ReceivedRecoveryShare {
final int messageId;
final String trustedFriendDisplayName;
final List<int>? trustedFriendAvatarSvg;
final String myDisplayName;
final int myUserId;
@ -58,7 +60,6 @@ class OnboardingState {
List<int>? encryptionKey;
bool emailRecoveryRequested;
List<ReceivedRecoveryShare> receivedShares = [];
Map<String, dynamic> toJson() => _$OnboardingStateToJson(this);

View file

@ -11,6 +11,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
) => ReceivedRecoveryShare(
messageId: (json['messageId'] as num).toInt(),
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
myDisplayName: json['myDisplayName'] as String,
myUserId: (json['myUserId'] as num).toInt(),
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
@ -27,6 +30,7 @@ Map<String, dynamic> _$ReceivedRecoveryShareToJson(
) => <String, dynamic>{
'messageId': instance.messageId,
'trustedFriendDisplayName': instance.trustedFriendDisplayName,
'trustedFriendAvatarSvg': instance.trustedFriendAvatarSvg,
'myDisplayName': instance.myDisplayName,
'myUserId': instance.myUserId,
'myAvatarSvg': instance.myAvatarSvg,

View file

@ -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();

View file

@ -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()

View file

@ -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<int>? 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,

View file

@ -364,7 +364,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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}',
);

View file

@ -543,6 +543,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
children: shares.map((share) {
return ContactChip(
username: share.trustedFriendDisplayName,
avatarSvg: share.trustedFriendAvatarSvg,
);
}).toList(),
),