mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
Warn in the chat when a peer's identity key changes
migrateNoiseKeyUpdate silently merged the DM thread onto the new key: a peer who panic-wiped (or was replaced by whoever holds the nickname) inherited the conversation's earned trust with no visible sign beyond a debug log, and any earlier verification — bound to the OLD fingerprint — vanished without a word. Signal treats this as a full-width banner; bitchat treated it as bookkeeping. The migration now appends a system line to the affected conversation: "<name>'s identity key changed — this can mean a new device or a reset. earlier verification no longer applies; verify them again before trusting this chat." Only when there is a conversation to protect (selected chat or migrated messages) — a favorite never chatted with doesn't spawn a warning-only thread. This is also the prerequisite UX for the peer-ID rotation project: once rotation ships, identity changes become routine and MUST be visible. 1 string x 30 locales; behavior pinned both ways by new coordinator tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0dacb3629c
commit
359139fc6b
@ -15997,6 +15997,192 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"system.identity.key_changed" : {
|
||||
"comment" : "Private-chat system warning after a peer's Noise identity key changed; placeholder is the peer's name",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "تغيّر مفتاح هوية %@ — قد يعني هذا جهازًا جديدًا أو إعادة ضبط. لم يعد التحقق السابق ساريًا؛ تحقّق من هويته مجددًا قبل الوثوق بهذه المحادثة."
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@-এর পরিচয় চাবি বদলে গেছে — এর মানে নতুন ডিভাইস বা রিসেট হতে পারে। আগের যাচাই আর প্রযোজ্য নয়; এই চ্যাটে ভরসা করার আগে তাকে আবার যাচাই করে নাও।"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "der identitätsschlüssel von %@ hat sich geändert — das kann ein neues gerät oder ein reset bedeuten. frühere verifizierung gilt nicht mehr; verifiziere die person erneut, bevor du diesem chat vertraust."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@'s identity key changed — this can mean a new device or a reset. earlier verification no longer applies; verify them again before trusting this chat."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "la clave de identidad de %@ cambió — puede significar un dispositivo nuevo o un restablecimiento. la verificación anterior ya no vale; verifica de nuevo su identidad antes de confiar en este chat."
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "کلید هویت %@ تغییر کرده — این میتواند به معنی دستگاه جدید یا بازنشانی باشد. تأیید قبلی دیگر معتبر نیست؛ پیش از اعتماد به این گفتگو دوباره هویتش را تأیید کن."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "nagbago ang identity key ni %@ — puwedeng ibig sabihin nito ay bagong device o reset. hindi na umiiral ang dating beripikasyon; i-verify siya ulit bago pagkatiwalaan ang chat na ito."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "la clé d'identité de %@ a changé — cela peut vouloir dire un nouvel appareil ou une réinitialisation. la vérification précédente ne vaut plus ; vérifie à nouveau son identité avant de faire confiance à cette conversation."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "מפתח הזהות של %@ השתנה — זה יכול להעיד על מכשיר חדש או איפוס. האימות הקודם כבר לא תקף; כדאי לאמת שוב לפני שסומכים על הצ'אט הזה."
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ की पहचान कुंजी बदल गई है — इसका मतलब नया डिवाइस या रीसेट हो सकता है। पहले किया गया सत्यापन अब मान्य नहीं; इस चैट पर भरोसा करने से पहले दोबारा सत्यापित कर लो।"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kunci identitas %@ berubah — bisa berarti perangkat baru atau reset. verifikasi sebelumnya tidak berlaku lagi; verifikasi dia lagi sebelum memercayai chat ini."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "la chiave d'identità di %@ è cambiata — può voler dire un nuovo dispositivo o un ripristino. la verifica precedente non vale più; verifica di nuovo la sua identità prima di fidarti di questa chat."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@のアイデンティティキーが変わりました — 新しい端末かリセットの可能性があります。以前の確認はもう有効ではありません。このチャットを信頼する前に、もう一度相手を確認してください。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@의 신원 키가 변경되었어요 — 새 기기나 초기화일 수 있어요. 이전 확인은 더 이상 유효하지 않으니, 이 채팅을 신뢰하기 전에 상대를 다시 확인해 주세요."
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kunci identiti %@ berubah — ini mungkin bermaksud peranti baharu atau tetapan semula. pengesahan sebelum ini tidak lagi terpakai; sahkan dia semula sebelum mempercayai sembang ini."
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ को पहिचान कुञ्जी परिवर्तन भयो — यसको अर्थ नयाँ डिभाइस वा रिसेट हुन सक्छ। पहिलेको प्रमाणीकरण अब लागू हुँदैन; यो च्याटमा भरोसा गर्नुअघि फेरि प्रमाणित गर।"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "de identiteitssleutel van %@ is veranderd — dat kan een nieuw apparaat of een reset betekenen. eerdere verificatie geldt niet meer; verifieer deze persoon opnieuw voordat je deze chat vertrouwt."
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "klucz tożsamości %@ się zmienił — to może oznaczać nowe urządzenie albo reset. wcześniejsza weryfikacja już nie obowiązuje; zweryfikuj tę osobę ponownie, zanim zaufasz temu czatowi."
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "a chave de identidade de %@ mudou — pode significar um dispositivo novo ou uma reposição. a verificação anterior já não se aplica; verifica outra vez a identidade antes de confiares neste chat."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "a chave de identidade de %@ mudou — pode significar um aparelho novo ou uma redefinição. a verificação anterior não vale mais; verifique de novo antes de confiar neste chat."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ключ идентификации %@ изменился — это может означать новое устройство или сброс. прежняя проверка больше не действует; проверь собеседника заново, прежде чем доверять этому чату."
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "identitetsnyckeln för %@ har ändrats — det kan betyda en ny enhet eller en återställning. tidigare verifiering gäller inte längre; verifiera personen igen innan du litar på den här chatten."
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ இன் அடையாளச் சாவி மாறிவிட்டது — இது புதிய சாதனம் அல்லது மீட்டமைப்பைக் குறிக்கலாம். முந்தைய சரிபார்ப்பு இனி செல்லாது; இந்த அரட்டையை நம்புவதற்கு முன் அவரை மீண்டும் சரிபார்த்துக்கொள்."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "กุญแจยืนยันตัวตนของ %@ เปลี่ยนไป — อาจหมายถึงอุปกรณ์ใหม่หรือการรีเซ็ต การยืนยันก่อนหน้านี้ใช้ไม่ได้อีกต่อไป ยืนยันตัวตนอีกครั้งก่อนไว้ใจแชทนี้"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ adlı kişinin kimlik anahtarı değişti — bu yeni bir cihaz ya da sıfırlama anlamına gelebilir. önceki doğrulama artık geçerli değil; bu sohbete güvenmeden önce onu yeniden doğrula."
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ключ ідентичності %@ змінився — це може означати новий пристрій або скидання. попередня перевірка більше не діє; перевір співрозмовника ще раз, перш ніж довіряти цьому чату."
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ کی شناختی کلید بدل گئی ہے — اس کا مطلب نیا آلہ یا ری سیٹ ہو سکتا ہے۔ پہلے کی تصدیق اب لاگو نہیں؛ اس چیٹ پر بھروسا کرنے سے پہلے دوبارہ تصدیق کرو۔"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "khóa định danh của %@ đã thay đổi — có thể là thiết bị mới hoặc do đặt lại. xác minh trước đây không còn hiệu lực; hãy xác minh lại trước khi tin tưởng cuộc trò chuyện này."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ 的身份密钥已更改 — 可能是换了新设备或进行了重置。之前的验证不再有效;在信任此聊天之前请重新验证对方。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ 的身分金鑰已變更 — 可能是換了新裝置或進行了重設。先前的驗證已不再有效;信任此聊天前請重新驗證對方。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.delivery.reason.legacy_media_consent_required" : {
|
||||
"comment" : "Failure reason when a legacy private-media send lacks per-send consent",
|
||||
"extractionState" : "manual",
|
||||
|
||||
@ -27,6 +27,10 @@ protocol ChatPeerIdentityContext: AnyObject {
|
||||
/// Moves all messages from `oldPeerID`'s chat into `newPeerID`'s chat
|
||||
/// (dedup by ID, order preserved, unread carried, old chat removed).
|
||||
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
|
||||
/// Appends a private message via the single-writer store intent
|
||||
/// (shared requirement with `ChatLifecycleContext`).
|
||||
@discardableResult
|
||||
func appendPrivateMessage(_ message: BitchatMessage, to peerID: PeerID) -> Bool
|
||||
var selectedPrivateChatPeer: PeerID? { get set }
|
||||
var selectedPrivateChatFingerprint: String? { get set }
|
||||
var myPeerID: PeerID { get }
|
||||
@ -578,7 +582,11 @@ private extension ChatPeerIdentityCoordinator {
|
||||
// order, carries the unread flag, and removes the old chat.
|
||||
context.migratePrivateChat(from: oldPeerID, to: newPeerID)
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatPeerIdentityCoordinator {
|
||||
// Internal (not in the private extension): the identity-changed warning
|
||||
// below is a security behavior the context tests pin directly.
|
||||
@MainActor
|
||||
func migrateNoiseKeyUpdate(oldPeerID: PeerID, newPeerID: PeerID) {
|
||||
// Capture before the migration: the store hands its selection off to
|
||||
@ -606,6 +614,30 @@ private extension ChatPeerIdentityCoordinator {
|
||||
context.selectedPrivateChatFingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
|
||||
// An identity-key change is a security event, not bookkeeping: any
|
||||
// earlier verification is bound to the OLD fingerprint and no longer
|
||||
// applies, and saying nothing would let whoever holds the new key
|
||||
// inherit the thread's earned trust under the same nickname. Only
|
||||
// warn when there is a conversation to protect.
|
||||
if wasSelected || !context.privateMessages(for: newPeerID).isEmpty {
|
||||
let notice = BitchatMessage(
|
||||
sender: "system",
|
||||
content: String(
|
||||
format: String(localized: "system.identity.key_changed", defaultValue: "%@'s identity key changed — this can mean a new device or a reset. earlier verification no longer applies; verify them again before trusting this chat.", comment: "Private-chat system warning after a peer's Noise identity key changed; placeholder is the peer's name"),
|
||||
locale: .current,
|
||||
resolveNickname(for: newPeerID)
|
||||
),
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: context.peerNickname(for: newPeerID),
|
||||
senderPeerID: context.myPeerID
|
||||
)
|
||||
context.appendPrivateMessage(notice, to: newPeerID)
|
||||
context.notifyUIChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -46,6 +46,15 @@ private final class MockChatPeerIdentityContext: ChatPeerIdentityContext {
|
||||
unreadPrivateMessages.remove(peerID)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func appendPrivateMessage(_ message: BitchatMessage, to peerID: PeerID) -> Bool {
|
||||
var chat = privateChats[peerID] ?? []
|
||||
guard !chat.contains(where: { $0.id == message.id }) else { return false }
|
||||
chat.append(message)
|
||||
privateChats[peerID] = chat
|
||||
return true
|
||||
}
|
||||
|
||||
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) {
|
||||
migratedChats.append((oldPeerID, newPeerID))
|
||||
guard oldPeerID != newPeerID, let source = privateChats[oldPeerID] else { return }
|
||||
@ -360,6 +369,44 @@ struct ChatPeerIdentityCoordinatorContextTests {
|
||||
#expect(context.cachedEncryptionStatuses[peerID] == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func migrateNoiseKeyUpdate_warnsThatIdentityChanged() async {
|
||||
let context = MockChatPeerIdentityContext()
|
||||
let coordinator = ChatPeerIdentityCoordinator(context: context)
|
||||
let oldPeerID = PeerID(str: "1111111111111111")
|
||||
let newPeerID = PeerID(str: "2222222222222222")
|
||||
context.nicknamesByPeerID[newPeerID] = "alice"
|
||||
context.privateChats[oldPeerID] = [makePrivateMessage(id: "m1", timestamp: Date(timeIntervalSince1970: 1))]
|
||||
|
||||
coordinator.migrateNoiseKeyUpdate(oldPeerID: oldPeerID, newPeerID: newPeerID)
|
||||
|
||||
// The thread migrated AND says out loud that the identity changed:
|
||||
// earlier verification is bound to the old fingerprint, and silence
|
||||
// would let whoever holds the new key inherit the earned trust.
|
||||
let migrated = context.privateChats[newPeerID] ?? []
|
||||
#expect(migrated.contains { $0.id == "m1" })
|
||||
let notice = migrated.last
|
||||
#expect(notice?.sender == "system")
|
||||
#expect(notice?.content.contains("identity key changed") == true)
|
||||
#expect(notice?.content.contains("alice") == true)
|
||||
#expect(context.privateChats[oldPeerID] == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func migrateNoiseKeyUpdate_staysQuietWithNoConversationToProtect() async {
|
||||
let context = MockChatPeerIdentityContext()
|
||||
let coordinator = ChatPeerIdentityCoordinator(context: context)
|
||||
|
||||
coordinator.migrateNoiseKeyUpdate(
|
||||
oldPeerID: PeerID(str: "3333333333333333"),
|
||||
newPeerID: PeerID(str: "4444444444444444")
|
||||
)
|
||||
|
||||
// No selected chat and no messages: a warning would create a
|
||||
// conversation out of nothing for a favorite never chatted with.
|
||||
#expect(context.privateChats.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func getEncryptionStatus_reflectsLiveSessionNotHistory() async {
|
||||
let context = MockChatPeerIdentityContext()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user