mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 01:14: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.myAvatarSvg,
|
||||||
required this.threshold,
|
required this.threshold,
|
||||||
required this.sharedSecretDataBytes,
|
required this.sharedSecretDataBytes,
|
||||||
|
this.trustedFriendAvatarSvg,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ReceivedRecoveryShare.fromJson(Map<String, dynamic> json) =>
|
factory ReceivedRecoveryShare.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|
@ -22,6 +23,7 @@ class ReceivedRecoveryShare {
|
||||||
final int messageId;
|
final int messageId;
|
||||||
|
|
||||||
final String trustedFriendDisplayName;
|
final String trustedFriendDisplayName;
|
||||||
|
final List<int>? trustedFriendAvatarSvg;
|
||||||
|
|
||||||
final String myDisplayName;
|
final String myDisplayName;
|
||||||
final int myUserId;
|
final int myUserId;
|
||||||
|
|
@ -58,7 +60,6 @@ class OnboardingState {
|
||||||
List<int>? encryptionKey;
|
List<int>? encryptionKey;
|
||||||
bool emailRecoveryRequested;
|
bool emailRecoveryRequested;
|
||||||
|
|
||||||
|
|
||||||
List<ReceivedRecoveryShare> receivedShares = [];
|
List<ReceivedRecoveryShare> receivedShares = [];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$OnboardingStateToJson(this);
|
Map<String, dynamic> toJson() => _$OnboardingStateToJson(this);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
|
||||||
) => ReceivedRecoveryShare(
|
) => ReceivedRecoveryShare(
|
||||||
messageId: (json['messageId'] as num).toInt(),
|
messageId: (json['messageId'] as num).toInt(),
|
||||||
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
|
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
|
||||||
|
trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
myDisplayName: json['myDisplayName'] as String,
|
myDisplayName: json['myDisplayName'] as String,
|
||||||
myUserId: (json['myUserId'] as num).toInt(),
|
myUserId: (json['myUserId'] as num).toInt(),
|
||||||
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
|
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
|
||||||
|
|
@ -27,6 +30,7 @@ Map<String, dynamic> _$ReceivedRecoveryShareToJson(
|
||||||
) => <String, dynamic>{
|
) => <String, dynamic>{
|
||||||
'messageId': instance.messageId,
|
'messageId': instance.messageId,
|
||||||
'trustedFriendDisplayName': instance.trustedFriendDisplayName,
|
'trustedFriendDisplayName': instance.trustedFriendDisplayName,
|
||||||
|
'trustedFriendAvatarSvg': instance.trustedFriendAvatarSvg,
|
||||||
'myDisplayName': instance.myDisplayName,
|
'myDisplayName': instance.myDisplayName,
|
||||||
'myUserId': instance.myUserId,
|
'myUserId': instance.myUserId,
|
||||||
'myAvatarSvg': instance.myAvatarSvg,
|
'myAvatarSvg': instance.myAvatarSvg,
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ class BackupService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) {
|
if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) {
|
||||||
// The KeyManager was restored sucessfully, restore the archive now.
|
// The KeyManager was restored successfully, restore the archive now.
|
||||||
try {
|
try {
|
||||||
final downloadToken =
|
final downloadToken =
|
||||||
await RustBackupArchive.getBackupDownloadToken();
|
await RustBackupArchive.getBackupDownloadToken();
|
||||||
|
|
|
||||||
|
|
@ -187,36 +187,29 @@ class PasswordlessRecoveryService {
|
||||||
|
|
||||||
switch (secondFactorType) {
|
switch (secondFactorType) {
|
||||||
case SecondFactorType.email:
|
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;
|
config.email = secondFactorValue;
|
||||||
emailHint = createEmailHint(secondFactorValue);
|
emailHint = createEmailHint(secondFactorValue);
|
||||||
|
|
||||||
// E-Mail Protection:
|
// E-Mail Protection:
|
||||||
// - Server can only learn the email during recovery. Ensured as the server gets the NONCE and MAC to decrpyt during recovery.
|
// - 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 send to the mail, they whould need access to the user's mail account.
|
// - Trusted-friends: Server key is only sent to the mail, they would need access to the user's mail account.
|
||||||
secondFactorEncryptedServerKeyKey = SecretKey(
|
secondFactorEncryptedServerKeyKey = SecretKey(
|
||||||
Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes),
|
Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes),
|
||||||
);
|
);
|
||||||
|
|
||||||
case SecondFactorType.pin:
|
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);
|
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
|
// 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 malicous user (except the trusted friends) to triger this deletion.
|
// unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion.
|
||||||
config.pinUnlockToken = getRandomUint8List(32);
|
config.pinUnlockToken = getRandomUint8List(32);
|
||||||
|
|
||||||
// Brute-force protection for the user's pin:
|
// Brute-force protection for the user's pin:
|
||||||
// - Server: Does not know the seed.
|
// - Server: Does not know the seed.
|
||||||
// - Trusted friends:
|
// - Trusted friends: Can only check the result 10 times before the server deletes the key. As they do not have
|
||||||
// Can only check the result X 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.
|
||||||
// the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10
|
|
||||||
// tries.
|
|
||||||
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||||
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
||||||
secretKey: SecretKey(config.pinSeed!),
|
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
|
// 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
|
// 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);
|
secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes);
|
||||||
|
|
||||||
case SecondFactorType.none:
|
case SecondFactorType.none:
|
||||||
|
|
@ -232,7 +225,7 @@ class PasswordlessRecoveryService {
|
||||||
|
|
||||||
if (secondFactorEncryptedServerKeyKey != null) {
|
if (secondFactorEncryptedServerKeyKey != null) {
|
||||||
// The server key is used to encrypt the RecoveryData of the users. This ensures that when the trusted friends
|
// 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);
|
serverKey = getRandomUint8List(32);
|
||||||
|
|
||||||
final secretBox = await xchacha20.encrypt(
|
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
|
// 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
|
// 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.
|
// brute-force the email of the user as he does not have the nonce.
|
||||||
encryptedServerKey = Uint8List.fromList([
|
encryptedServerKey = Uint8List.fromList([
|
||||||
...secretBox.cipherText,
|
...secretBox.cipherText,
|
||||||
|
|
@ -323,7 +316,7 @@ class PasswordlessRecoveryService {
|
||||||
|
|
||||||
unawaited(performHeartbeat());
|
unawaited(performHeartbeat());
|
||||||
|
|
||||||
// The passwordless is configured sucessfully.
|
// The passwordless is configured successfully.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,7 +389,7 @@ class PasswordlessRecoveryService {
|
||||||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
||||||
|
|
||||||
if (isContactHeartbeatOlderThan24h) {
|
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.
|
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
|
||||||
final pendingShares =
|
final pendingShares =
|
||||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||||
|
|
@ -542,7 +535,7 @@ class PasswordlessRecoveryService {
|
||||||
final recoveryLastHeartbeat =
|
final recoveryLastHeartbeat =
|
||||||
const ListEquality().equals(computedHash, msg.hash)
|
const ListEquality().equals(computedHash, msg.hash)
|
||||||
? clock.now()
|
? 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(
|
Log.info(
|
||||||
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
|
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
|
||||||
);
|
);
|
||||||
|
|
@ -602,6 +595,9 @@ class PasswordlessRecoveryService {
|
||||||
final receivedShare = ReceivedRecoveryShare(
|
final receivedShare = ReceivedRecoveryShare(
|
||||||
messageId: msgId,
|
messageId: msgId,
|
||||||
trustedFriendDisplayName: share.trustedFriend.displayName,
|
trustedFriendDisplayName: share.trustedFriend.displayName,
|
||||||
|
trustedFriendAvatarSvg: share.trustedFriend.hasAvatar()
|
||||||
|
? share.trustedFriend.avatar
|
||||||
|
: null,
|
||||||
myDisplayName: share.shareUser.displayName,
|
myDisplayName: share.shareUser.displayName,
|
||||||
myUserId: share.shareUser.userId.toInt(),
|
myUserId: share.shareUser.userId.toInt(),
|
||||||
myAvatarSvg: share.shareUser.hasAvatar()
|
myAvatarSvg: share.shareUser.hasAvatar()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
|
|
@ -12,6 +15,7 @@ class ContactChip extends StatelessWidget {
|
||||||
const ContactChip({
|
const ContactChip({
|
||||||
this.contact,
|
this.contact,
|
||||||
this.username,
|
this.username,
|
||||||
|
this.avatarSvg,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
|
|
@ -21,6 +25,7 @@ class ContactChip extends StatelessWidget {
|
||||||
|
|
||||||
final Contact? contact;
|
final Contact? contact;
|
||||||
final String? username;
|
final String? username;
|
||||||
|
final List<int>? avatarSvg;
|
||||||
final void Function(int)? onTap;
|
final void Function(int)? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -43,6 +48,9 @@ class ContactChip extends StatelessWidget {
|
||||||
avatar: AvatarIcon(
|
avatar: AvatarIcon(
|
||||||
contactId: contact?.userId,
|
contactId: contact?.userId,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
svg: avatarSvg != null
|
||||||
|
? getAvatarSvg(Uint8List.fromList(avatarSvg!))
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
label: Row(
|
label: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
return advanceToNextMediaOrExit();
|
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(
|
Log.info(
|
||||||
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -543,6 +543,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
children: shares.map((share) {
|
children: shares.map((share) {
|
||||||
return ContactChip(
|
return ContactChip(
|
||||||
username: share.trustedFriendDisplayName,
|
username: share.trustedFriendDisplayName,
|
||||||
|
avatarSvg: share.trustedFriendAvatarSvg,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue