notification of the verified user

This commit is contained in:
otsmr 2026-04-22 03:32:14 +02:00
parent e1f28e1b87
commit 7d09bd7283
34 changed files with 957 additions and 270 deletions

View file

@ -12,6 +12,7 @@ import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
@ -121,15 +122,25 @@ Future<void> runMigrations() async {
if (userService.currentUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await updateUser((u) {
u.appVersion = 90;
});
await updateUser((u) => u.appVersion = 90);
}
if (userService.currentUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91();
await updateUser((u) {
u.appVersion = 91;
});
await updateUser((u) => u.appVersion = 91);
}
if (userService.currentUser.appVersion < 109) {
final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) {
if (contact.verified) {
await twonlyDB.keyVerificationDao.addKeyVerification(
contact.userId,
VerificationType.migratedFromOldVersion,
);
}
}
await updateUser((u) => u.appVersion = 109);
}
}

View file

@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
part 'contacts.dao.g.dart';
@DriftAccessor(tables: [Contacts])
@DriftAccessor(tables: [Contacts, KeyVerifications])
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -99,6 +99,21 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
}
Stream<(Contact, bool)?> watchContactAndVerificationState(int userid) {
final query = (select(contacts)..where((t) => t.userId.equals(userid))).join([
leftOuterJoin(
keyVerifications,
keyVerifications.contactId.equalsExp(contacts.userId),
),
]);
return query
.map((row) => (
row.readTable(contacts),
row.readTableOrNull(keyVerifications) != null,
))
.watchSingleOrNull();
}
Future<List<Contact>> getAllContacts() {
return select(contacts).get();
}

View file

@ -5,6 +5,8 @@ part of 'contacts.dao.dart';
// ignore_for_file: type=lint
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
ContactsDaoManager get managers => ContactsDaoManager(this);
}
@ -13,4 +15,9 @@ class ContactsDaoManager {
ContactsDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
}

View file

@ -0,0 +1,81 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'key_verification.dao.g.dart';
@DriftAccessor(
tables: [Contacts, VerificationTokens, KeyVerifications, GroupMembers],
)
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
with _$KeyVerificationDaoMixin {
// ignore: matching_super_parameters
KeyVerificationDao(super.db);
Future<List<VerificationToken>> getRecentVerificationTokens() {
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
return (select(
verificationTokens,
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
}
Future<int> insertVerificationToken(Uint8List token) {
return into(verificationTokens).insert(
VerificationTokensCompanion.insert(token: token),
);
}
/// Returns a map of contactId the verification type of the earliest
/// [KeyVerification] row for that contact.
Future<Map<int, VerificationType>>
getFirstVerificationTypeByContacts() async {
final rows = await (select(
keyVerifications,
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
final result = <int, VerificationType>{};
for (final row in rows) {
result.putIfAbsent(row.contactId, () => row.type);
}
return result;
}
Future<bool> isContactVerified(int contactId) async {
final row =
await (select(keyVerifications)
..where((kv) => kv.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
return row != null;
}
Stream<List<KeyVerification>> watchContactVerification(int contactId) {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).watch();
}
Stream<bool> watchAllGroupMembersVerified(String groupId) {
final gm = groupMembers;
final kv = keyVerifications;
final query = (select(gm)..where((m) => m.groupId.equals(groupId))).join([
leftOuterJoin(kv, kv.contactId.equalsExp(gm.contactId)),
]);
return query.watch().map(
(rows) =>
rows.isNotEmpty && rows.every((r) => r.readTableOrNull(kv) != null),
);
}
Future<void> addKeyVerification(int contactId, VerificationType type) async {
await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion(
contactId: Value(contactId),
type: Value(type),
),
);
}
}

View file

@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'key_verification.dao.dart';
// ignore_for_file: type=lint
mixin _$KeyVerificationDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$VerificationTokensTable get verificationTokens =>
attachedDatabase.verificationTokens;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
$GroupsTable get groups => attachedDatabase.groups;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
}
class KeyVerificationDaoManager {
final _$KeyVerificationDaoMixin _db;
KeyVerificationDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$VerificationTokensTableTableManager get verificationTokens =>
$$VerificationTokensTableTableManager(
_db.attachedDatabase,
_db.verificationTokens,
);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$GroupMembersTableTableManager get groupMembers =>
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
}

View file

@ -38,8 +38,11 @@ class Contacts extends Table {
}
enum VerificationType {
qr,
migratedFromOldVersion,
qrScanned,
link,
secretQrToken,
contactSharedByVerified,
}
@DataClassName('KeyVerification')

View file

@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart'
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart';
@ -60,6 +61,7 @@ part 'twonly.db.g.dart';
ReactionsDao,
MediaFilesDao,
UserDiscoveryDao,
KeyVerificationDao,
],
)
class TwonlyDB extends _$TwonlyDB {

View file

@ -11374,6 +11374,9 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
late final UserDiscoveryDao userDiscoveryDao = UserDiscoveryDao(
this as TwonlyDB,
);
late final KeyVerificationDao keyVerificationDao = KeyVerificationDao(
this as TwonlyDB,
);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();

View file

@ -970,6 +970,42 @@ abstract class AppLocalizations {
/// **'Clear verification'**
String get contactVerifyNumberClearVerification;
/// No description provided for @userVerifiedTitle.
///
/// In en, this message translates to:
/// **'User verified'**
String get userVerifiedTitle;
/// No description provided for @verificationTypeQrScanned.
///
/// In en, this message translates to:
/// **'You scanned their QR code.'**
String get verificationTypeQrScanned;
/// No description provided for @verificationTypeSecretQrToken.
///
/// In en, this message translates to:
/// **'The other person scanned your QR code.'**
String get verificationTypeSecretQrToken;
/// No description provided for @verificationTypeLink.
///
/// In en, this message translates to:
/// **'Verified via link.'**
String get verificationTypeLink;
/// No description provided for @verificationTypeContactSharedByVerified.
///
/// In en, this message translates to:
/// **'Contact received from a verified contact.'**
String get verificationTypeContactSharedByVerified;
/// No description provided for @verificationTypeMigratedFromOldVersion.
///
/// In en, this message translates to:
/// **'Migrated from old version.'**
String get verificationTypeMigratedFromOldVersion;
/// No description provided for @contactVerifyNumberLongDesc.
///
/// In en, this message translates to:

View file

@ -482,6 +482,27 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contactVerifyNumberClearVerification => 'Verifizierung aufheben';
@override
String get userVerifiedTitle => 'Benutzer verifiziert';
@override
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
@override
String get verificationTypeSecretQrToken =>
'Die andere Person hat deinen QR-Code gescannt.';
@override
String get verificationTypeLink => 'Per Link verifiziert.';
@override
String get verificationTypeContactSharedByVerified =>
'Von einem verifizierten Kontakt geteilt bekommen.';
@override
String get verificationTypeMigratedFromOldVersion =>
'Von alter Version migriert';
@override
String contactVerifyNumberLongDesc(Object username) {
return 'Um die Ende-zu-Ende-Verschlüsselung mit $username zu verifizieren, vergleiche die Zahlen mit deren Gerät. Die Person kann auch deinen Code mit deren Gerät scannen.';

View file

@ -477,6 +477,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contactVerifyNumberClearVerification => 'Clear verification';
@override
String get userVerifiedTitle => 'User verified';
@override
String get verificationTypeQrScanned => 'You scanned their QR code.';
@override
String get verificationTypeSecretQrToken =>
'The other person scanned your QR code.';
@override
String get verificationTypeLink => 'Verified via link.';
@override
String get verificationTypeContactSharedByVerified =>
'Contact received from a verified contact.';
@override
String get verificationTypeMigratedFromOldVersion =>
'Migrated from old version.';
@override
String contactVerifyNumberLongDesc(Object username) {
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';

View file

@ -477,6 +477,27 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contactVerifyNumberClearVerification => 'Clear verification';
@override
String get userVerifiedTitle => 'User verified';
@override
String get verificationTypeQrScanned => 'You scanned their QR code.';
@override
String get verificationTypeSecretQrToken =>
'The other person scanned your QR code.';
@override
String get verificationTypeLink => 'Verified via link.';
@override
String get verificationTypeContactSharedByVerified =>
'Contact received from a verified contact.';
@override
String get verificationTypeMigratedFromOldVersion =>
'Migrated from old version.';
@override
String contactVerifyNumberLongDesc(Object username) {
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';

@ -1 +1 @@
Subproject commit 57ec512977e514fca6413622bb4a7e03701f09a0
Subproject commit a35f6a65cf87db6e1b48dea1c0260b59b52b21f1

View file

@ -1857,6 +1857,68 @@ class EncryptedContent_UserDiscoveryUpdate extends $pb.GeneratedMessage {
$pb.PbList<$core.List<$core.int>> get messages => $_getList(0);
}
class EncryptedContent_KeyVerificationProof extends $pb.GeneratedMessage {
factory EncryptedContent_KeyVerificationProof({
$core.List<$core.int>? calculatedMac,
}) {
final result = create();
if (calculatedMac != null) result.calculatedMac = calculatedMac;
return result;
}
EncryptedContent_KeyVerificationProof._();
factory EncryptedContent_KeyVerificationProof.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_KeyVerificationProof.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.KeyVerificationProof',
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'calculatedMac', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_KeyVerificationProof clone() =>
EncryptedContent_KeyVerificationProof()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_KeyVerificationProof copyWith(
void Function(EncryptedContent_KeyVerificationProof) updates) =>
super.copyWith((message) =>
updates(message as EncryptedContent_KeyVerificationProof))
as EncryptedContent_KeyVerificationProof;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_KeyVerificationProof create() =>
EncryptedContent_KeyVerificationProof._();
@$core.override
EncryptedContent_KeyVerificationProof createEmptyInstance() => create();
static $pb.PbList<EncryptedContent_KeyVerificationProof> createRepeated() =>
$pb.PbList<EncryptedContent_KeyVerificationProof>();
@$core.pragma('dart2js:noInline')
static EncryptedContent_KeyVerificationProof getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
EncryptedContent_KeyVerificationProof>(create);
static EncryptedContent_KeyVerificationProof? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get calculatedMac => $_getN(0);
@$pb.TagNumber(1)
set calculatedMac($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasCalculatedMac() => $_has(0);
@$pb.TagNumber(1)
void clearCalculatedMac() => $_clearField(1);
}
class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({
$core.String? groupId,
@ -1881,6 +1943,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
$core.List<$core.int>? senderUserDiscoveryVersion,
EncryptedContent_UserDiscoveryRequest? userDiscoveryRequest,
EncryptedContent_UserDiscoveryUpdate? userDiscoveryUpdate,
EncryptedContent_KeyVerificationProof? keyVerificationProof,
}) {
final result = create();
if (groupId != null) result.groupId = groupId;
@ -1911,6 +1974,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
result.userDiscoveryRequest = userDiscoveryRequest;
if (userDiscoveryUpdate != null)
result.userDiscoveryUpdate = userDiscoveryUpdate;
if (keyVerificationProof != null)
result.keyVerificationProof = keyVerificationProof;
return result;
}
@ -1979,6 +2044,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_UserDiscoveryUpdate>(
23, _omitFieldNames ? '' : 'userDiscoveryUpdate',
subBuilder: EncryptedContent_UserDiscoveryUpdate.create)
..aOM<EncryptedContent_KeyVerificationProof>(
24, _omitFieldNames ? '' : 'keyVerificationProof',
subBuilder: EncryptedContent_KeyVerificationProof.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2251,6 +2319,19 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(23)
EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() =>
$_ensure(21);
@$pb.TagNumber(24)
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(22);
@$pb.TagNumber(24)
set keyVerificationProof(EncryptedContent_KeyVerificationProof value) =>
$_setField(24, value);
@$pb.TagNumber(24)
$core.bool hasKeyVerificationProof() => $_has(22);
@$pb.TagNumber(24)
void clearKeyVerificationProof() => $_clearField(24);
@$pb.TagNumber(24)
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
$_ensure(22);
}
const $core.bool _omitFieldNames =

View file

@ -365,6 +365,16 @@ const EncryptedContent$json = {
'10': 'userDiscoveryUpdate',
'17': true
},
{
'1': 'key_verification_proof',
'3': 24,
'4': 1,
'5': 11,
'6': '.EncryptedContent.KeyVerificationProof',
'9': 22,
'10': 'keyVerificationProof',
'17': true
},
],
'3': [
EncryptedContent_ErrorMessages$json,
@ -384,7 +394,8 @@ const EncryptedContent$json = {
EncryptedContent_FlameSync$json,
EncryptedContent_TypingIndicator$json,
EncryptedContent_UserDiscoveryRequest$json,
EncryptedContent_UserDiscoveryUpdate$json
EncryptedContent_UserDiscoveryUpdate$json,
EncryptedContent_KeyVerificationProof$json
],
'8': [
{'1': '_group_id'},
@ -409,6 +420,7 @@ const EncryptedContent$json = {
{'1': '_typing_indicator'},
{'1': '_user_discovery_request'},
{'1': '_user_discovery_update'},
{'1': '_key_verification_proof'},
],
};
@ -911,6 +923,14 @@ const EncryptedContent_UserDiscoveryUpdate$json = {
],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_KeyVerificationProof$json = {
'1': 'KeyVerificationProof',
'2': [
{'1': 'calculated_mac', '3': 1, '4': 1, '5': 12, '10': 'calculatedMac'},
],
};
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh4KCGdyb3VwX2lkGAIgASgJSABSB2dyb3VwSWSIAQESKQoOaX'
@ -941,75 +961,79 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'gTUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEoCzIm'
'LkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFFIUdXNlckRpc2NvdmVyeV'
'JlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVkQ29u'
'dGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBVSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQEa8AEKDU'
'Vycm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2'
'VpcHRJZCJ3CgRUeXBlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09V'
'TlRfUkVRVUVTVF9JTlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU'
'9OX09VVF9PRl9TWU5DEAMaVAoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0'
'ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRo1CglHcm91cE'
'pvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5k'
'R3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgAS'
'gJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZl'
'Y3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbW'
'WIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIi'
'bmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udG'
'FjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJf'
'bWlsbGlzZWNvbmRzGq8BCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCV'
'IPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgD'
'Ugl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZI'
'gBAUITChFfcXVvdGVfbWVzc2FnZV9pZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNl'
'bmRlcl9tZXNzYWdlX2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgAS'
'gDUgl0aW1lc3RhbXASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2Vf'
'ZGF0YRgEIAEoDEgAUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbW'
'Vzc2FnZV9kYXRhGmQKCFJlYWN0aW9uEioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJn'
'ZXRNZXNzYWdlSWQSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3'
'ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1l'
'c3NhZ2VVcGRhdGUuVHlwZVIEdHlwZRIvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW'
'5kZXJNZXNzYWdlSWSIAQESPQobbXVsdGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhht'
'dWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbW'
'VzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhU'
'EAESCgoGT1BFTkVEEAJCFAoSX3NlbmRlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYR'
'IqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiAB'
'KA4yHC5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF'
'9pbl9taWxsaXNlY29uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQES'
'NwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb2'
'4SHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEo'
'CUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG'
'9hZFRva2VuiAEBEioKDmVuY3J5cHRpb25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQES'
'KgoOZW5jcnlwdGlvbl9tYWMYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW'
'9uX25vbmNlGAogASgMSAVSD2VuY3J5cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3Nh'
'Z2VfZGF0YRgLIAEoDEgGUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRV'
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9k'
'aXNwbGF5X2xpbWl0X2luX21pbGxpc2Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG'
'93bmxvYWRfdG9rZW5CEQoPX2VuY3J5cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFf'
'ZW5jcnlwdGlvbl9ub25jZUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVX'
'BkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVS'
'BHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeX'
'BlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNv'
'bnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZX'
'F1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUND'
'RVBUEAIapAIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbn'
'QuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEo'
'DEgAUhNhdmF0YXJTdmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW'
'1liAEBEiYKDGRpc3BsYXlfbmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsK'
'B1JFUVVFU1QQABIKCgZVUERBVEUQAUIYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2'
'VybmFtZUIPCg1fZGlzcGxheV9uYW1lGtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5j'
'cnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZX'
'lJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3Jl'
'YXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBg'
'oEX2tleUINCgtfY3JlYXRlZF9hdBqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASAB'
'KANSDGZsYW1lQ291bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYX'
'N0RmxhbWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEK'
'DGZvcmNlX3VwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCW'
'lzX3R5cGluZxgBIAEoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0'
'Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3Vycm'
'VudFZlcnNpb24aMQoTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVz'
'c2FnZXNCCwoJX2dyb3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbG'
'VfY291bnRlckIgCh5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CEQoPX21lc3NhZ2Vf'
'dXBkYXRlQggKBl9tZWRpYUIPCg1fbWVkaWFfdXBkYXRlQhEKD19jb250YWN0X3VwZGF0ZUISCh'
'BfY29udGFjdF9yZXF1ZXN0Qg0KC19mbGFtZV9zeW5jQgwKCl9wdXNoX2tleXNCCwoJX3JlYWN0'
'aW9uQg8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg'
'1fZ3JvdXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVz'
'c2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQh'
'kKF191c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGU=');
'dGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBVSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQoWa2'
'V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVyaWZp'
'Y2F0aW9uUHJvb2ZIFlIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQEa8AEKDUVycm9yTWVzc2FnZX'
'MSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUgR0'
'eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3CgRUeX'
'BlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVUVTVF9J'
'TlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9PRl9TWU'
'5DEAMaVAoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91'
'cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRo1CglHcm91cEpvaW4SKAoQZ3JvdX'
'BfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNL'
'ZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdG'
'lvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJ'
'ZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2'
'RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVz'
'c2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbm'
'V3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRz'
'Gq8BCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2'
'FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXAS'
'LQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdG'
'VfbWVzc2FnZV9pZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdl'
'X2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbX'
'ASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgA'
'UhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGm'
'QKCFJlYWN0aW9uEioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQS'
'FAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYW'
'dlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUu'
'VHlwZVIEdHlwZRIvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSW'
'SIAQESPQobbXVsdGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdl'
'dE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1'
'IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVE'
'EAJCFAoSX3NlbmRlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbW'
'Vzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0'
'ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY2'
'9uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNf'
'YXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW'
'1wGAUgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1l'
'c3NhZ2VJZIgBARIqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
'oKDmVuY3J5cHRpb25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlv'
'bl9tYWMYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogAS'
'gMSAVSD2VuY3J5cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEo'
'DEgGUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBU'
'lNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0'
'X2luX21pbGxpc2Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW'
'5CEQoPX2VuY3J5cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9u'
'b25jZUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cG'
'UYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFy'
'Z2V0X21lc3NhZ2VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTk'
'VEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0'
'EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBH'
'R5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNv'
'bnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZG'
'F0ZS5UeXBlUgR0eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT'
'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3'
'BsYXlfbmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIK'
'CgZVUERBVEUQAUIYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZG'
'lzcGxheV9uYW1lGtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVu'
'dC5QdXNoS2V5cy5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZX'
'kYAyABKAxIAVIDa2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8K'
'BFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3'
'JlYXRlZF9hdBqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291'
'bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudG'
'VyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0'
'ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIA'
'EoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNj'
'b3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQ'
'oTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5'
'VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYW'
'NCCwoJX2dyb3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbGVfY291'
'bnRlckIgCh5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CEQoPX21lc3NhZ2VfdXBkYX'
'RlQggKBl9tZWRpYUIPCg1fbWVkaWFfdXBkYXRlQhEKD19jb250YWN0X3VwZGF0ZUISChBfY29u'
'dGFjdF9yZXF1ZXN0Qg0KC19mbGFtZV9zeW5jQgwKCl9wdXNoX2tleXNCCwoJX3JlYWN0aW9uQg'
'8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg1fZ3Jv'
'dXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVzc2FnZX'
'NCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQhkKF191'
'c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGVCGQoXX2tleV'
'92ZXJpZmljYXRpb25fcHJvb2Y=');

View file

@ -99,6 +99,7 @@ class PublicProfile extends $pb.GeneratedMessage {
$fixnum.Int64? registrationId,
$core.List<$core.int>? signedPrekeySignature,
$fixnum.Int64? signedPrekeyId,
$core.List<$core.int>? secretVerificationToken,
}) {
final result = create();
if (userId != null) result.userId = userId;
@ -109,6 +110,8 @@ class PublicProfile extends $pb.GeneratedMessage {
if (signedPrekeySignature != null)
result.signedPrekeySignature = signedPrekeySignature;
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
if (secretVerificationToken != null)
result.secretVerificationToken = secretVerificationToken;
return result;
}
@ -134,6 +137,8 @@ class PublicProfile extends $pb.GeneratedMessage {
..a<$core.List<$core.int>>(
6, _omitFieldNames ? '' : 'signedPrekeySignature', $pb.PbFieldType.OY)
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
..a<$core.List<$core.int>>(
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -220,6 +225,16 @@ class PublicProfile extends $pb.GeneratedMessage {
$core.bool hasSignedPrekeyId() => $_has(6);
@$pb.TagNumber(7)
void clearSignedPrekeyId() => $_clearField(7);
@$pb.TagNumber(8)
$core.List<$core.int> get secretVerificationToken => $_getN(7);
@$pb.TagNumber(8)
set secretVerificationToken($core.List<$core.int> value) =>
$_setBytes(7, value);
@$pb.TagNumber(8)
$core.bool hasSecretVerificationToken() => $_has(7);
@$pb.TagNumber(8)
void clearSecretVerificationToken() => $_clearField(8);
}
const $core.bool _omitFieldNames =

View file

@ -67,6 +67,18 @@ const PublicProfile$json = {
'10': 'signedPrekeySignature'
},
{'1': 'signed_prekey_id', '3': 7, '4': 1, '5': 3, '10': 'signedPrekeyId'},
{
'1': 'secret_verification_token',
'3': 8,
'4': 1,
'5': 12,
'9': 0,
'10': 'secretVerificationToken',
'17': true
},
],
'8': [
{'1': '_secret_verification_token'},
],
};
@ -77,4 +89,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
'dHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EicKD3JlZ2lzdHJhdG'
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
'5zaWduZWRQcmVrZXlJZA==');
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');

View file

@ -56,6 +56,7 @@ message EncryptedContent {
optional TypingIndicator typing_indicator = 20;
optional UserDiscoveryRequest user_discovery_request = 22;
optional UserDiscoveryUpdate user_discovery_update = 23;
optional KeyVerificationProof key_verification_proof = 24;
message ErrorMessages {
enum Type {
@ -209,4 +210,8 @@ message EncryptedContent {
repeated bytes messages = 1;
}
message KeyVerificationProof {
bytes calculated_mac = 1;
}
}

View file

@ -16,4 +16,5 @@ message PublicProfile {
int64 registration_id = 5;
bytes signed_prekey_signature = 6;
int64 signed_prekey_id = 7;
optional bytes secret_verification_token = 8;
}

View file

@ -29,6 +29,7 @@ import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
@ -330,6 +331,14 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null);
}
if (content.hasKeyVerificationProof()) {
await KeyVerificationService.handleVerificationProof(
fromUserId,
content.keyVerificationProof.calculatedMac,
);
return (null, null);
}
if (content.hasMediaUpdate()) {
await handleMediaUpdate(
fromUserId,

View file

@ -4,15 +4,14 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
@ -70,22 +69,19 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
return true;
}
if (storedPublicKey.equals(receivedPublicKey)) {
if (!contact.verified) {
final markAsVerified = await showAlertDialog(
context,
context.lang.linkFromUsername(contact.username),
context.lang.linkFromUsernameLong,
customOk: context.lang.gotLinkFromFriend,
final markAsVerified = await showAlertDialog(
context,
context.lang.linkFromUsername(contact.username),
context.lang.linkFromUsernameLong,
customOk: context.lang.gotLinkFromFriend,
);
if (markAsVerified) {
await twonlyDB.keyVerificationDao.addKeyVerification(
contact.userId,
VerificationType.link,
);
if (markAsVerified) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
verified: Value(true),
),
);
}
} else {
}
if (context.mounted) {
await context.push(Routes.profileContact(contact.userId));
}
} else {

View file

@ -0,0 +1,120 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class KeyVerificationService {
static Future<List<int>> getNewSecretVerificationToken() async {
final token = getRandomUint8List(16);
await twonlyDB.keyVerificationDao.insertVerificationToken(token);
return token.toList();
}
static Future<void> handleScannedVerificationToken(
int contactId,
Uint8List contactPubKey,
List<int> secretToken,
) async {
Log.info('Notifying verified user using the secret token');
final calculatedMac = await _createVerificationBytes(
contactId,
contactPubKey,
secretToken,
false,
);
await sendCipherText(
contactId,
pb.EncryptedContent(
keyVerificationProof: pb.EncryptedContent_KeyVerificationProof(
calculatedMac: calculatedMac,
),
),
);
}
static Future<void> handleVerificationProof(
int fromUserId,
List<int> receivedMac,
) async {
Log.info('Received a verification proof. Verifying the calculated mac...');
final contactPubKey = await getPublicKeyFromContact(fromUserId);
if (contactPubKey == null) {
Log.error('No public key stored..');
return;
}
final secretTokens = await twonlyDB.keyVerificationDao
.getRecentVerificationTokens();
for (final secretToken in secretTokens) {
final recalculatedMac = await _createVerificationBytes(
fromUserId,
contactPubKey,
secretToken.token,
true,
);
if (recalculatedMac.equals(receivedMac)) {
await twonlyDB.keyVerificationDao.addKeyVerification(
fromUserId,
VerificationType.secretQrToken,
);
Log.info('Contact was verified via secretQrToken');
return;
}
}
Log.error('No valid secret token could be found...');
}
}
Future<List<int>> _createVerificationBytes(
int contactId,
Uint8List contactPubKey,
List<int> secretToken,
bool verifying,
) async {
final bytes = <int>[];
final userPublicKey = await getUserPublicKey();
final ownBytes = [
..._userIdToLeBytes(userService.currentUser.userId),
...userPublicKey,
];
final contactBytes = [..._userIdToLeBytes(contactId), ...contactPubKey];
if (verifying) {
bytes
..addAll(ownBytes)
..addAll(contactBytes);
} else {
bytes
..addAll(contactBytes)
..addAll(ownBytes);
}
final hmac = Hmac.sha256();
final mac = await hmac.calculateMac(
Uint8List.fromList(bytes),
secretKey: SecretKey(secretToken),
);
return mac.bytes;
}
List<int> _userIdToLeBytes(int userId) {
final byteData = ByteData(8)..setInt64(0, userId, Endian.little);
return byteData.buffer.asUint8List();
}

View file

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -91,6 +92,12 @@ Future<SignalIdentity?> getSignalIdentity() async {
}
}
Future<Uint8List> getUserPublicKey() async {
final signalIdentity = (await getSignalIdentity())!;
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize();
}
Future<void> createIfNotExistsSignalIdentity() async {
const storage = FlutterSecureStorage();

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
@ -7,9 +6,9 @@ import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/qr.dart';
class UserDiscoveryService {
static Future<void> checkForNewAnnouncedUsers() async {

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
@ -43,11 +44,28 @@ Future<void> handleUserStudyUpload() async {
}
final contacts = await twonlyDB.contactsDao.getAllContacts();
final verifications = await twonlyDB.keyVerificationDao
.getFirstVerificationTypeByContacts();
final dataCollection = {
'total_contacts': contacts.length,
'accepted_contacts': contacts.where((c) => c.accepted).length,
'verified_contacts': contacts.where((c) => c.verified).length,
'verified_contacts': verifications.length,
'verified_contacts_via_migrated_from_old_version': verifications.values
.where((c) => c == VerificationType.migratedFromOldVersion)
.length,
'verified_contacts_via_qr_scanned': verifications.values
.where((c) => c == VerificationType.qrScanned)
.length,
'verified_contacts_via_link': verifications.values
.where((c) => c == VerificationType.link)
.length,
'verified_contacts_via_secret_qr_token': verifications.values
.where((c) => c == VerificationType.secretQrToken)
.length,
'verified_contacts_via_contact_shared_by_verified': verifications.values
.where((c) => c == VerificationType.contactSharedByVerified)
.length,
};
final response = await http.post(

View file

@ -1,59 +1,114 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart' show ListExtensions;
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
Future<Uint8List> getProfileQrCodeData() async {
final signalIdentity = (await getSignalIdentity())!;
class QrCodeUtils {
static String linkPrefix = 'https://me.twonly.eu/qr/#';
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
static Future<String> publicProfileLink() async {
final signalIdentity = (await getSignalIdentity())!;
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
final publicProfile = PublicProfile(
userId: Int64(userService.currentUser.userId),
username: userService.currentUser.username,
publicIdentityKey: (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize(),
registrationId: Int64(signalIdentity.registrationId),
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
signedPrekeySignature: signedPreKey.signature,
signedPrekeyId: Int64(signedPreKey.id),
);
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final data = publicProfile.writeToBuffer();
final secretVerificationToken =
await KeyVerificationService.getNewSecretVerificationToken();
final qrEnvelope = QREnvelope(
type: QREnvelope_Type.PUBLIC_PROFILE,
data: data,
);
final publicProfile = PublicProfile(
userId: Int64(userService.currentUser.userId),
username: userService.currentUser.username,
publicIdentityKey: (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize(),
registrationId: Int64(signalIdentity.registrationId),
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
signedPrekeySignature: signedPreKey.signature,
signedPrekeyId: Int64(signedPreKey.id),
secretVerificationToken: secretVerificationToken,
);
return qrEnvelope.writeToBuffer();
}
final data = publicProfile.writeToBuffer();
Future<Uint8List> getUserPublicKey() async {
final signalIdentity = (await getSignalIdentity())!;
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize();
}
final qrEnvelope = QREnvelope(
type: QREnvelope_Type.PUBLIC_PROFILE,
data: data,
);
PublicProfile? parseQrCodeData(Uint8List rawBytes) {
try {
final envelop = QREnvelope.fromBuffer(rawBytes);
if (envelop.type == QREnvelope_Type.PUBLIC_PROFILE) {
return PublicProfile.fromBuffer(envelop.data);
}
} catch (e) {
// Log.warn(e);
final bytes = qrEnvelope.writeToBuffer();
final urlSafeBase64 = base64Url.encode(bytes);
return '$linkPrefix$urlSafeBase64';
}
// returns: profile, NEW_USER=true/VERIFIED_USER=false, VERIFICATION_OK
static Future<(PublicProfile, Contact?, bool)?> handleQrCodeLink(
String link,
) async {
late PublicProfile profile;
try {
final bytes = base64Url.decode(link.replaceFirst(linkPrefix, ''));
final envelope = QREnvelope.fromBuffer(bytes);
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) return null;
profile = PublicProfile.fromBuffer(envelope.data);
} catch (e) {
Log.error(e);
return null;
}
final contact = await twonlyDB.contactsDao.getContactById(
profile.userId.toInt(),
);
if (contact == null || !contact.accepted) {
if (profile.username == userService.currentUser.username) {
return null;
}
// NEW_USER
return (profile, null, false);
}
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
if (storedPublicKey == null) return null;
final verificationOk = profile.publicIdentityKey.equals(
storedPublicKey.toList(),
);
if (verificationOk) {
if (profile.hasSecretVerificationToken()) {
unawaited(
KeyVerificationService.handleScannedVerificationToken(
contact.userId,
storedPublicKey,
profile.secretVerificationToken,
),
);
}
await twonlyDB.keyVerificationDao.addKeyVerification(
contact.userId,
VerificationType.qrScanned,
);
}
return (profile, contact, verificationOk);
}
return null;
}
Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
@ -72,14 +127,28 @@ Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
requested: const Value(false),
blocked: const Value(false),
deletedByUser: const Value(false),
verified: const Value(
true,
), // This contact was added from a QR-Code scan, so the public key was not loaded from the server
),
);
// The user was added via the profile scanned from the QR code so the scanned public key was used.
await twonlyDB.keyVerificationDao.addKeyVerification(
profile.userId.toInt(),
VerificationType.qrScanned,
);
if (added > 0) {
return importSignalContactAndCreateRequest(userdata);
if (await importSignalContactAndCreateRequest(userdata)) {
if (profile.hasSecretVerificationToken()) {
await KeyVerificationService.handleScannedVerificationToken(
profile.userId.toInt(),
Uint8List.fromList(profile.publicIdentityKey),
profile.secretVerificationToken,
);
}
return true;
}
return false;
}
return false;
}

View file

@ -28,27 +28,29 @@ class VerificationBadgeComp extends StatefulWidget {
}
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
bool isVerified = false;
Contact? contact;
bool _isVerified = false;
StreamSubscription<List<Contact>>? stream;
StreamSubscription<bool>? _streamAllVerified;
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
@override
void initState() {
if (widget.group != null) {
stream = twonlyDB.groupsDao
.watchGroupContact(widget.group!.groupId)
.listen((contacts) {
if (contacts.length == 1) {
contact = contacts.first;
}
_streamAllVerified = twonlyDB.keyVerificationDao
.watchAllGroupMembersVerified(widget.group!.groupId)
.listen((update) {
setState(() {
isVerified = contacts.every((t) => t.verified);
_isVerified = update;
});
});
} else if (widget.contact != null) {
isVerified = widget.contact!.verified;
contact = widget.contact;
_streamContactVerification = twonlyDB.keyVerificationDao
.watchContactVerification(widget.contact!.userId)
.listen((update) {
setState(() {
_isVerified = update.isNotEmpty;
});
});
}
super.initState();
@ -56,15 +58,16 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
@override
void dispose() {
stream?.cancel();
_streamAllVerified?.cancel();
_streamContactVerification?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!isVerified && widget.showOnlyIfVerified) return Container();
if (!_isVerified && widget.showOnlyIfVerified) return Container();
return GestureDetector(
onTap: (contact == null || !widget.clickable)
onTap: (!widget.clickable)
? null
: () => context.push(Routes.settingsHelpFaqVerifyBadge),
child: ColoredBox(
@ -77,7 +80,7 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
bottom: 3,
),
child: SvgIcon(
assetPath: isVerified
assetPath: _isVerified
? SvgIcons.verifiedGreen
: SvgIcons.verifiedRed,
size: widget.size,

View file

@ -3,8 +3,6 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -16,7 +14,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.dart';
@ -48,6 +45,7 @@ class MainCameraController {
bool initCameraStarted = true;
Map<int, ScannedVerifiedContact> contactsVerified = {};
Map<int, ScannedNewProfile> scannedNewProfiles = {};
Set<String> _handledProfileLinks = {};
String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey();
@ -340,70 +338,56 @@ class MainCameraController {
}
for (final barcode in barcodes) {
if (barcode.displayValue != null) {
if (barcode.displayValue!.startsWith('http://') ||
barcode.displayValue!.startsWith('https://')) {
scannedUrl = barcode.displayValue;
if (sharedLinkForPreview == null) {
timeSharedLinkWasSetWithQr = clock.now();
setSharedLinkForPreview(Uri.parse(scannedUrl!));
}
}
}
if (barcode.rawBytes == null) continue;
if (barcode.displayValue == null) continue;
final link = barcode.displayValue!;
final profile = parseQrCodeData(barcode.rawBytes!);
if (link.startsWith(QrCodeUtils.linkPrefix)) {
if (_handledProfileLinks.contains(link)) continue;
_handledProfileLinks.add(link);
if (profile == null) continue;
final res = await QrCodeUtils.handleQrCodeLink(link);
if (res == null) continue;
final (profile, contact, verificationOk) = res;
final contact = await twonlyDB.contactsDao.getContactById(
profile.userId.toInt(),
);
if (contact != null && contact.accepted) {
if (contactsVerified[contact.userId] == null) {
final storedPublicKey = await getPublicKeyFromContact(
contact.userId,
);
if (storedPublicKey != null) {
final verificationOk = profile.publicIdentityKey.equals(
storedPublicKey.toList(),
);
contactsVerified[contact.userId] = ScannedVerifiedContact(
contact: contact,
verificationOk: verificationOk,
);
if (verificationOk) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(verified: Value(true)),
);
}
await HapticFeedback.heavyImpact();
if (verificationOk) {
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
),
);
}
}
}
} else {
if (profile.username != userService.currentUser.username) {
if (contact == null) {
if (scannedNewProfiles[profile.userId.toInt()] == null) {
await HapticFeedback.heavyImpact();
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
profile: profile,
);
}
continue;
}
if (contactsVerified[contact.userId] == null) {
contactsVerified[contact.userId] = ScannedVerifiedContact(
contact: contact,
verificationOk: verificationOk,
);
await HapticFeedback.heavyImpact();
if (verificationOk) {
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
),
);
}
}
}
if (link.startsWith('http://') || link.startsWith('https://')) {
scannedUrl = link;
if (sharedLinkForPreview == null) {
timeSharedLinkWasSetWithQr = clock.now();
setSharedLinkForPreview(Uri.parse(scannedUrl!));
}
}
}

View file

@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/misc.dart';
@ -130,13 +131,26 @@ class _SearchUsernameView extends State<AddNewUserView> {
requested: const Value(false),
blocked: const Value(false),
deletedByUser: const Value(false),
verified: Value(
!(widget.publicKey == null) &&
userdata.publicIdentityKey.equals(widget.publicKey!),
),
),
);
if (widget.publicKey != null &&
mounted &&
widget.publicKey!.equals(userdata.publicIdentityKey)) {
final markAsVerified = await showAlertDialog(
context,
context.lang.linkFromUsername(username),
context.lang.linkFromUsernameLong,
customOk: context.lang.gotLinkFromFriend,
);
if (markAsVerified) {
await twonlyDB.keyVerificationDao.addKeyVerification(
userdata.userId.toInt(),
VerificationType.link,
);
}
}
if (added > 0) await importSignalContactAndCreateRequest(userdata);
}

View file

@ -1,11 +1,13 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
@ -120,13 +122,18 @@ class _ContactRowState extends State<_ContactRow> {
);
if (userdata == null) return;
var verified = false;
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
final sender = await twonlyDB.contactsDao.getContactById(
if (userdata.publicIdentityKey.equals(widget.contact.publicIdentityKey)) {
final verified = await twonlyDB.keyVerificationDao.isContactVerified(
widget.message.senderId!,
);
// in case the sender is verified and the public keys are the same, this trust can be transferred
verified = sender != null && sender.verified;
if (verified) {
Log.info('Verified a user which was shared by a verified contact');
await twonlyDB.keyVerificationDao.addKeyVerification(
userdata.userId.toInt(),
VerificationType.contactSharedByVerified,
);
}
}
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
@ -136,9 +143,6 @@ class _ContactRowState extends State<_ContactRow> {
requested: const Value(false),
blocked: const Value(false),
deletedByUser: const Value(false),
verified: Value(
verified,
),
),
);

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
import 'package:mutex/mutex.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:photo_view/photo_view.dart';
import 'package:twonly/locator.dart';
@ -103,43 +104,47 @@ class _MediaViewerViewState extends State<MediaViewerView> {
super.dispose();
}
final Mutex _messageUpdateLock = Mutex();
Future<void> asyncLoadNextMedia(bool firstRun) async {
final messages = twonlyDB.messagesDao.watchMediaNotOpened(
widget.group.groupId,
);
_subscription = messages.listen((messages) async {
for (final msg in messages) {
if (_alreadyOpenedMediaIds.contains(msg.mediaId)) {
continue;
}
if (msg.mediaId == null) {
continue;
}
await _messageUpdateLock.protect(() async {
for (final msg in messages) {
if (_alreadyOpenedMediaIds.contains(msg.mediaId)) {
continue;
}
if (msg.mediaId == null) {
continue;
}
if (msg.mediaId == currentMedia?.mediaFile.mediaId) {
// The update of the current Media in case of a download is done in loadCurrentMediaFile
continue;
if (msg.mediaId == currentMedia?.mediaFile.mediaId) {
// The update of the current Media in case of a download is done in loadCurrentMediaFile
continue;
}
/// If the messages was already there just replace it and go to the next...
final index = allMediaFiles.indexWhere(
(m) => m.messageId == msg.messageId,
);
if (index >= 1) {
allMediaFiles[index] = msg;
} else if (index == -1) {
// If the message does not exist, add it
allMediaFiles.add(msg);
}
}
/// If the messages was already there just replace it and go to the next...
final index = allMediaFiles.indexWhere(
(m) => m.messageId == msg.messageId,
);
if (index >= 1) {
allMediaFiles[index] = msg;
} else if (index == -1) {
// If the message does not exist, add it
allMediaFiles.add(msg);
setState(() {});
if (firstRun) {
firstRun = false;
await loadCurrentMediaFile();
}
}
setState(() {});
if (firstRun) {
firstRun = false;
await loadCurrentMediaFile();
}
});
});
}

View file

@ -1,13 +1,16 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -30,24 +33,38 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> {
Contact? _contact;
List<GroupMember> _memberOfGroups = [];
List<KeyVerification> _keyVerifications = [];
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<(Contact, bool)?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub;
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
@override
void initState() {
_contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
update,
) {
setState(() {
_contact = update;
});
});
_contactSub = twonlyDB.contactsDao
.watchContactAndVerificationState(widget.userId)
.listen((
update,
) {
if (update != null) {
setState(() {
_contact = update.$1;
});
}
});
_groupMemberSub = twonlyDB.groupsDao
.watchContactGroupMember(widget.userId)
.listen((groups) async {
_memberOfGroups = groups;
});
_streamKeyVerifications = twonlyDB.keyVerificationDao
.watchContactVerification(widget.userId)
.listen((update) {
setState(() {
Log.info('Verifications: ${update.length}');
_keyVerifications = update;
});
});
super.initState();
}
@ -55,6 +72,7 @@ class _ContactViewState extends State<ContactView> {
void dispose() {
_contactSub.cancel();
_groupMemberSub.cancel();
_streamKeyVerifications.cancel();
super.dispose();
}
@ -214,7 +232,7 @@ class _ContactViewState extends State<ContactView> {
RestoreFlameComp(
contactId: widget.userId,
),
if (!contact.verified)
if (_keyVerifications.isEmpty)
BetterListTile(
leading: VerificationBadgeComp(
contact: contact,
@ -226,6 +244,37 @@ class _ContactViewState extends State<ContactView> {
setState(() {});
},
),
if (_keyVerifications.isNotEmpty)
ExpansionTile(
shape: const RoundedRectangleBorder(),
backgroundColor: context.color.surfaceContainer,
collapsedShape: const RoundedRectangleBorder(),
leading: Padding(
padding: EdgeInsetsGeometry.only(left: 12, right: 12),
child: VerificationBadgeComp(
contact: contact,
size: 20,
),
),
title: Text(context.lang.userVerifiedTitle),
children: _keyVerifications
.map(
(kv) => ListTile(
dense: true,
title: Text(_verificationTypeLabel(context, kv.type)),
trailing: Text(
DateFormat.yMd(
Localizations.localeOf(context).toString(),
).format(kv.createdAt),
style: TextStyle(
color: context.color.onSurfaceVariant,
fontSize: 13,
),
),
),
)
.toList(),
),
if (userService.currentUser.isUserDiscoveryEnabled)
BetterListTile(
icon: FontAwesomeIcons.usersViewfinder,
@ -279,6 +328,19 @@ class _ContactViewState extends State<ContactView> {
}
}
String _verificationTypeLabel(BuildContext context, VerificationType type) {
return switch (type) {
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
VerificationType.secretQrToken =>
context.lang.verificationTypeSecretQrToken,
VerificationType.link => context.lang.verificationTypeLink,
VerificationType.contactSharedByVerified =>
context.lang.verificationTypeContactSharedByVerified,
VerificationType.migratedFromOldVersion =>
context.lang.verificationTypeMigratedFromOldVersion,
};
}
Future<String?> showNicknameChangeDialog(
BuildContext context,
Contact contact,

View file

@ -8,6 +8,7 @@ import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.dart';
@ -21,7 +22,7 @@ class PublicProfileView extends StatefulWidget {
}
class _PublicProfileViewState extends State<PublicProfileView> {
Uint8List? _qrCode;
String? _qrCode;
Uint8List? _userAvatar;
Uint8List? _publicKey;
@ -32,7 +33,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
}
Future<void> initAsync() async {
_qrCode = await getProfileQrCodeData();
_qrCode = await QrCodeUtils.publicProfileLink();
_userAvatar = await getUserAvatar();
_publicKey = await getUserPublicKey();
if (mounted) setState(() {});
@ -82,7 +83,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
],
),
child: QrImageView.withQr(
qr: QrCode.fromUint8List(
qr: QrCode.fromData(
data: _qrCode!,
errorCorrectLevel: QrErrorCorrectLevel.M,
),

View file

@ -72,7 +72,7 @@ class _PrivacyViewState extends State<PrivacyView> {
},
),
ListTile(
title: const Text('Freunde finden'),
title: Text(context.lang.userDiscoverySettingsTitle),
onTap: () async {
await context.push(Routes.settingsPrivacyUserDiscovery);
setState(() {});