fix(security): treat a new identity key as a message request, never as a known contact
A stranger could pick the display name of an existing contact ("Mom")
and appear as that contact. Messages from a key that is not in the
contact list now open a message request labelled "(unknown)": no
replies, read receipts, typing indicators, calls or name in
notifications until the user accepts it and names the contact
themselves. Names that look like an existing contact (case, diacritics,
Cyrillic/Greek homoglyphs, invisible characters) also show a warning in
the real contact's chat.
When the relay account of a known contact shows up with a different
key, the chat shows "safety number changed", verification is cleared
and the session that now belongs to the other key is dropped before
sending, so a reply never goes to the new key.
This commit is contained in:
@@ -26,9 +26,19 @@ class ContactsNotifier extends AsyncNotifier<List<Contact>> {
|
||||
throw const ContactLinkIncompleteError();
|
||||
}
|
||||
|
||||
// Kontrola duplicity
|
||||
if (contacts.any((c) => c.publicIdentity.id == identity.id)) {
|
||||
throw const ContactAlreadyExistsError();
|
||||
// Kontrola duplicity. Výjimka: žádost o zprávu od téhož klíče — ruční
|
||||
// přidání (QR, odkaz) je právě to přijetí, na které čekala.
|
||||
final existing =
|
||||
contacts.where((c) => c.publicIdentity.id == identity.id).firstOrNull;
|
||||
if (existing != null) {
|
||||
if (existing.accepted) throw const ContactAlreadyExistsError();
|
||||
await accept(existing.id,
|
||||
name: identity.displayName.isEmpty
|
||||
? existing.displayName
|
||||
: identity.displayName);
|
||||
if (verified) existing.verified = true;
|
||||
await storage.saveContact(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
final contact = Contact.fromPublicIdentity(identity, verified: verified);
|
||||
@@ -38,6 +48,20 @@ class ContactsNotifier extends AsyncNotifier<List<Contact>> {
|
||||
return contact;
|
||||
}
|
||||
|
||||
/// Přijme žádost o zprávu pod jménem, které jí dal uživatel (jméno
|
||||
/// zvolené odesílatelem se tím nepřebírá automaticky).
|
||||
Future<void> accept(String contactId, {required String name}) async {
|
||||
final storage = ref.read(storageProvider);
|
||||
final contacts = await future;
|
||||
final contact = contacts.where((c) => c.id == contactId).firstOrNull;
|
||||
if (contact == null) return;
|
||||
contact
|
||||
..displayName = name.trim()
|
||||
..accepted = true;
|
||||
await storage.saveContact(contact);
|
||||
state = AsyncData([...contacts]);
|
||||
}
|
||||
|
||||
/// Smaž kontakt
|
||||
Future<void> removeContact(String contactId) async {
|
||||
final storage = ref.read(storageProvider);
|
||||
|
||||
@@ -341,8 +341,10 @@ class ConversationsNotifier extends AsyncNotifier<List<Conversation>> {
|
||||
|
||||
// Příchozí zprávy, které jsem právě přečetl → pošlu za ně potvrzení
|
||||
// přečtení (sendReadReceipts sám zkontroluje, jestli je funkce zapnutá).
|
||||
// Upozornění appky (notice) nikdo neposlal — potvrzovat ho nelze.
|
||||
final justRead = conv.messages
|
||||
.where((m) => !m.isMine(identity.id) && !m.isRead)
|
||||
.where((m) =>
|
||||
!m.isMine(identity.id) && !m.isRead && m.notice == null)
|
||||
.toList();
|
||||
|
||||
conv.markAllRead(identity.id);
|
||||
|
||||
@@ -35,9 +35,14 @@ final avatarProvider =
|
||||
final me = ref.watch(identityProvider).valueOrNull;
|
||||
final contacts = ref.watch(contactsProvider).valueOrNull ?? const [];
|
||||
if (!service.isStarted) return null;
|
||||
final contact = contacts.where((c) => c.id == fingerprint).firstOrNull;
|
||||
// Fotka neznámého odesílatele může být fotka mámy z Facebooku — do
|
||||
// přijetí jen iniciály na tónu z klíče, ne jeho obrázek.
|
||||
final avatar = fingerprint == me?.id
|
||||
? service.myAvatar
|
||||
: contacts.where((c) => c.id == fingerprint).firstOrNull?.avatar;
|
||||
: contact != null && contact.accepted
|
||||
? contact.avatar
|
||||
: null;
|
||||
return avatar == null ? null : service.loadAvatar(avatar);
|
||||
});
|
||||
|
||||
@@ -83,7 +88,15 @@ final messagingStartProvider = FutureProvider<void>((ref) async {
|
||||
final conv = await ref.read(storageProvider).loadConversation(convId);
|
||||
final mentionedMe = message.mentions.contains(identity.id);
|
||||
if (conv?.isMuted == true && !mentionedMe) return;
|
||||
PushNotifier.showMessageNotification(contact.displayName);
|
||||
// Jméno, které si neznámý zvolil sám, nepatří do notifikace —
|
||||
// „Nová zpráva od Máma“ by byl phishing ještě před otevřením appky.
|
||||
if (contact.accepted) {
|
||||
PushNotifier.showMessageNotification(contact.displayName);
|
||||
} else if (convId != contact.id) {
|
||||
PushNotifier.showWakeNotification();
|
||||
} else {
|
||||
PushNotifier.showMessageRequestNotification();
|
||||
}
|
||||
};
|
||||
|
||||
service.onCallSignal = (contact, signal) =>
|
||||
|
||||
Reference in New Issue
Block a user