mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 02:24:07 +00:00
fix comments and show shared contacts avatar
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
This commit is contained in:
parent
1b37268577
commit
df9b0d66bd
7 changed files with 33 additions and 23 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -543,6 +543,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
|||
children: shares.map((share) {
|
||||
return ContactChip(
|
||||
username: share.trustedFriendDisplayName,
|
||||
avatarSvg: share.trustedFriendAvatarSvg,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue