diff --git a/bitchat/App/VerificationModel.swift b/bitchat/App/VerificationModel.swift index 4e1de457..66fbe794 100644 --- a/bitchat/App/VerificationModel.swift +++ b/bitchat/App/VerificationModel.swift @@ -63,6 +63,14 @@ final class VerificationModel: ObservableObject { return VerificationService.shared.buildMyQRString(nickname: currentNickname, npub: npub) ?? "" } + func identityVerificationCard() -> IdentityVerificationCard { + IdentityVerificationCard.current( + nickname: currentNickname, + npub: try? chatViewModel.idBridge.getCurrentNostrIdentity()?.npub, + noiseFingerprint: chatViewModel.getMyFingerprint() + ) + } + func verifyScannedPayload(_ payload: String) -> VerificationScanOutcome { guard let qr = VerificationService.shared.verifyScannedQR(payload) else { return .invalid diff --git a/bitchat/Identity/IdentityVerificationCard.swift b/bitchat/Identity/IdentityVerificationCard.swift new file mode 100644 index 00000000..1c68b33c --- /dev/null +++ b/bitchat/Identity/IdentityVerificationCard.swift @@ -0,0 +1,71 @@ +// +// IdentityVerificationCard.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +/// Public-only identity card for out-of-band verification (#183). +/// +/// Contains nickname, npub, and Noise fingerprint — never private key material. +/// Passphrase-sealed backup export remains future work; this card is the safe +/// first step for sharing who you are on bitchat. +struct IdentityVerificationCard: Equatable { + let nickname: String + let npub: String? + let noiseFingerprint: String + + var plainText: String { + var lines: [String] = [ + String( + localized: "identity.card.title", + defaultValue: "bitchat identity card", + comment: "Title line when exporting a public identity verification card" + ), + "", + String( + format: String( + localized: "identity.card.nickname", + defaultValue: "nickname: %@", + comment: "Nickname line on the public identity card; %@ is the nickname" + ), + locale: .current, + nickname + ), + String( + format: String( + localized: "identity.card.fingerprint", + defaultValue: "noise fingerprint: %@", + comment: "Fingerprint line on the public identity card; %@ is the hex fingerprint" + ), + locale: .current, + noiseFingerprint + ) + ] + if let npub, !npub.isEmpty { + lines.append( + String( + format: String( + localized: "identity.card.npub", + defaultValue: "npub: %@", + comment: "Nostr npub line on the public identity card; %@ is the npub" + ), + locale: .current, + npub + ) + ) + } + return lines.joined(separator: "\n") + } + + static func current(nickname: String, npub: String?, noiseFingerprint: String) -> IdentityVerificationCard { + IdentityVerificationCard( + nickname: nickname, + npub: npub, + noiseFingerprint: noiseFingerprint + ) + } +} diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index e498a20e..6e1b8b7d 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -13393,6 +13393,936 @@ } } }, + "identity.card.copy" : { + "comment" : "Button that copies the public identity verification card as plain text", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نسخ بطاقة الهوية" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পরিচয় কার্ড কপি করুন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "identitätskarte kopieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "copy identity card" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "copiar tarjeta de identidad" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "کپی کارت هویت" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "kopyahin ang identity card" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "copier la carte d'identité" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העתקת כרטיס הזהות" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पहचान कार्ड कॉपी करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "salin kartu identitas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "copia scheda identità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "身元カードをコピー" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "신원 카드 복사" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "salin kad identiti" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "परिचय कार्ड प्रतिलिपि गर्नुहोस्" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "identiteitskaart kopiëren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "kopiuj kartę tożsamości" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "copiar cartão de identidade" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "copiar cartão de identidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "копировать карточку личности" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "kopiera identitetskortet" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அடையாள அட்டையை நகலெடு" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอกบัตรประจำตัว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "kimlik kartını kopyala" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "копіювати картку особи" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "شناختی کارڈ کاپی کریں" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "sao chép thẻ danh tính" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制身份卡" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製身分卡" + } + } + } + }, + "identity.card.fingerprint" : { + "comment" : "Fingerprint line on the public identity card; %@ is the hex fingerprint", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بصمة noise: %@" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise ফিঙ্গারপ্রিন্ট: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise-fingerabdruck: %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise fingerprint: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "huella noise: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "اثر انگشت noise: %@" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "fingerprint ng noise: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "empreinte noise : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "טביעת אצבע noise: %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise फ़िंगरप्रिंट: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "sidik jari noise: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "impronta noise: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise フィンガープリント:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise 지문: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "cap jari noise: %@" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise फिंगरप्रिन्ट: %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise-vingerafdruk: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "odcisk noise: %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "impressão digital noise: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "impressão digital noise: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "отпечаток noise: %@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise-fingeravtryck: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise கைரேகை: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ลายนิ้วมือ noise: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise parmak izi: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "відбиток noise: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise فنگر پرنٹ: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "dấu vân tay noise: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise 指纹:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "noise 指紋:%@" + } + } + } + }, + "identity.card.nickname" : { + "comment" : "Nickname line on the public identity card; %@ is the nickname", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الاسم المستعار: %@" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ডাকনাম: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "spitzname: %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "nickname: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "apodo: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "نام مستعار: %@" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "palayaw: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "pseudo : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "כינוי: %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उपनाम: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "nama panggilan: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "soprannome: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニックネーム:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닉네임: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "nama panggilan: %@" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "उपनाम: %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "bijnaam: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "pseudonim: %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "apelido: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "apelido: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "псевдоним: %@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "smeknamn: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "புனைப்பெயர்: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ชื่อเล่น: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "takma ad: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "псевдонім: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "عرفی نام: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "biệt danh: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "昵称:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "暱稱:%@" + } + } + } + }, + "identity.card.npub" : { + "comment" : "Nostr npub line on the public identity card; %@ is the npub", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "npub:%@" + } + } + } + }, + "identity.card.title" : { + "comment" : "Title line when exporting a public identity verification card", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بطاقة هوية bitchat" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat পরিচয় কার্ড" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat-identitätskarte" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat identity card" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tarjeta de identidad de bitchat" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "کارت هویت bitchat" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "identity card ng bitchat" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "carte d'identité bitchat" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "כרטיס זהות של bitchat" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat पहचान कार्ड" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "kartu identitas bitchat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "scheda identità bitchat" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat 身元カード" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat 신원 카드" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "kad identiti bitchat" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat परिचय कार्ड" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat-identiteitskaart" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "karta tożsamości bitchat" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "cartão de identidade bitchat" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "cartão de identidade do bitchat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "карточка личности bitchat" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat-identitetskort" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat அடையாள அட்டை" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "บัตรประจำตัว bitchat" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat kimlik kartı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "картка особи bitchat" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat شناختی کارڈ" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "thẻ danh tính bitchat" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat 身份卡" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat 身分卡" + } + } + } + }, "image_picker.none_selected" : { "comment" : "Placeholder shown in the image picker before a selection", "extractionState" : "manual", diff --git a/bitchat/Views/VerificationViews.swift b/bitchat/Views/VerificationViews.swift index a3f9d82b..26d7a0ab 100644 --- a/bitchat/Views/VerificationViews.swift +++ b/bitchat/Views/VerificationViews.swift @@ -460,6 +460,17 @@ struct VerificationSheetView: View { // Centered controls moved up VStack(spacing: 10) { + if !showingScanner { + Button(action: copyIdentityVerificationCard) { + Label( + String(localized: "identity.card.copy", defaultValue: "copy identity card", comment: "Button that copies the public identity verification card as plain text"), + systemImage: "doc.on.doc" + ) + .bitchatFont(size: 13) + } + .buttonStyle(.bordered) + } + if showingScanner { Button(action: { showingScanner = false }) { Label("show my qr", systemImage: "qrcode") @@ -492,4 +503,15 @@ struct VerificationSheetView: View { .themedSheetBackground() .onDisappear { showingScanner = false } } + + private func copyIdentityVerificationCard() { + let text = verificationModel.identityVerificationCard().plainText + #if os(iOS) + UIPasteboard.general.string = text + #else + let pb = NSPasteboard.general + pb.clearContents() + pb.setString(text, forType: .string) + #endif + } } diff --git a/bitchatTests/IdentityVerificationCardTests.swift b/bitchatTests/IdentityVerificationCardTests.swift new file mode 100644 index 00000000..469809b8 --- /dev/null +++ b/bitchatTests/IdentityVerificationCardTests.swift @@ -0,0 +1,69 @@ +// +// IdentityVerificationCardTests.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Testing +@testable import bitchat + +struct IdentityVerificationCardTests { + @Test func plainTextIncludesPublicFieldsOnly() { + let card = IdentityVerificationCard.current( + nickname: "alice", + npub: "npub1test", + noiseFingerprint: "abcd1234" + ) + let text = card.plainText + #expect(text.contains("alice")) + #expect(text.contains("npub1test")) + #expect(text.contains("abcd1234")) + #expect(!text.lowercased().contains("private key")) + #expect(!text.lowercased().contains("seed")) + } + + /// The signed QR URL is deliberately absent. `verifyScannedQR` rejects a + /// payload older than `verificationQRMaxAgeSeconds` (5 minutes), so a URL + /// on a card people keep and forward would stop verifying almost at once + /// while still reading as the card's most authoritative line. + @Test func plainTextOmitsTheExpiringVerificationURL() { + let text = IdentityVerificationCard.current( + nickname: "alice", + npub: "npub1test", + noiseFingerprint: "abcd1234" + ).plainText + #expect(!text.contains("bitchat://")) + #expect(!text.lowercased().contains("scan to verify")) + } + + /// No npub yet means no npub line — not an empty one. + @Test func plainTextOmitsTheNpubLineWhenThereIsNoNpub() { + let withNpub = IdentityVerificationCard.current( + nickname: "alice", + npub: "npub1test", + noiseFingerprint: "abcd1234" + ).plainText + let withoutNpub = IdentityVerificationCard.current( + nickname: "alice", + npub: nil, + noiseFingerprint: "abcd1234" + ).plainText + #expect(withNpub.contains("npub1test")) + #expect(!withoutNpub.contains("npub")) + #expect(withoutNpub.contains("alice")) + #expect(withoutNpub.contains("abcd1234")) + } + + /// An empty npub string is the same as none — an empty line would look + /// like a missing key rather than an absent identity. + @Test func plainTextTreatsAnEmptyNpubAsAbsent() { + let text = IdentityVerificationCard.current( + nickname: "alice", + npub: "", + noiseFingerprint: "abcd1234" + ).plainText + #expect(!text.contains("npub")) + } +} diff --git a/docs/IDENTITY-VERIFICATION-CARD.md b/docs/IDENTITY-VERIFICATION-CARD.md new file mode 100644 index 00000000..49d8da0c --- /dev/null +++ b/docs/IDENTITY-VERIFICATION-CARD.md @@ -0,0 +1,31 @@ +# Identity verification card export (#183) + +## Scope (this PR) + +Public **identity verification card** export only: + +- Nickname +- Noise fingerprint +- Nostr npub (when available) + +Available from the verification sheet via **copy identity card**. + +## Why the signed QR URL is not on the card + +The verification sheet's QR encodes a signed `bitchat://verify?...` URL, and it +would be the natural thing to put on a text card. It is deliberately left off: +`VerificationService.verifyScannedQR` rejects a payload older than +`TransportConfig.verificationQRMaxAgeSeconds` (5 minutes), so a URL copied onto +a card people keep and forward would stop verifying almost immediately — while +still reading as the most authoritative line on it. + +The fingerprint and npub have no expiry, which is what makes them the right +fields for an out-of-band card. Scan the QR for a live, signed exchange; use the +card when the other person is not in front of a screen. + +## Out of scope (future) + +- Passphrase-sealed Noise identity backup / restore (see open PR #1520) +- Private key or seed export in any form + +The card gives people a safe, shareable way to confirm who they are out-of-band before marking a fingerprint verified.