import Foundation import CryptoKit /// Contact key verification: fingerprints, safety numbers, QR codes. /// Matches Python: crypto_utils.py compute_fingerprint, compute_safety_number, etc. enum ContactVerification { /// Version byte for fingerprint computation (Signal's NumericFingerprint). private static let fingerprintVersion: UInt16 = 0 /// Number of SHA-512 iterations for fingerprint computation. private static let fingerprintIterations = 5200 // MARK: - Fingerprint /// Compute a 32-byte fingerprint for a user's identity key. /// /// Uses iterated SHA-512 (Signal's NumericFingerprint algorithm). /// Seed: version(2B big-endian) + identity_key(32B) + user_id(UTF-8). /// Each iteration: SHA-512(previous_hash + identity_key). /// Output: first 32 bytes of final hash. static func computeFingerprint(userId: String, identityKey: Data, iterations: Int = fingerprintIterations) -> Data { let versionBytes = fingerprintVersion.bigEndianData var data = versionBytes + identityKey + Data(userId.utf8) for _ in 0.. String { var groups: [String] = [] for i in 0..<6 { let start = i * 5 let end = min(start + 5, fpBytes.count) let slice = fpBytes[fpBytes.startIndex + start ..< fpBytes.startIndex + end] let num = bigEndianUInt64(slice) % 100000 groups.append(String(format: "%05d", num)) } return groups[0..<3].joined(separator: " ") + "\n" + groups[3..<6].joined(separator: " ") } // MARK: - Safety Number /// Compute a 60-digit safety number for a pair of users. /// /// Both users see the same number regardless of who computes it. /// Lower user_id's fingerprint comes first (deterministic ordering). /// Output: 12 groups of 5 digits, formatted as 3 lines of 4 groups. static func computeSafetyNumber( myUserId: String, myIdentityKey: Data, theirUserId: String, theirIdentityKey: Data ) -> String { let fpMine = computeFingerprint(userId: myUserId, identityKey: myIdentityKey) let fpTheirs = computeFingerprint(userId: theirUserId, identityKey: theirIdentityKey) let combined: Data if myUserId < theirUserId { combined = fpMine + fpTheirs } else { combined = fpTheirs + fpMine } // 64 bytes -> 12 groups of 5 digits var groups: [String] = [] for i in 0..<12 { let start = i * 5 let end = min(start + 5, combined.count) let slice = combined[combined.startIndex + start ..< combined.startIndex + end] let num = bigEndianUInt64(slice) % 100000 groups.append(String(format: "%05d", num)) } return [ groups[0..<4].joined(separator: " "), groups[4..<8].joined(separator: " "), groups[8..<12].joined(separator: " "), ].joined(separator: "\n") } // MARK: - QR Code /// Encode user identity for QR code verification. /// /// Format: version(1B=0x01) + uid_len(1B) + uid(UTF-8) + identity_key(32B). static func encodeVerificationQR(userId: String, identityKey: Data) -> Data { let uidBytes = Data(userId.utf8) var data = Data([0x01, UInt8(uidBytes.count)]) data.append(uidBytes) data.append(identityKey) return data } /// Decode QR code verification payload. /// /// Returns (userId, identityKey). /// Throws on invalid format. static func decodeVerificationQR(_ data: Data) throws -> (userId: String, identityKey: Data) { guard data.count >= 3 else { throw VerificationError.qrDataTooShort } guard data[data.startIndex] == 0x01 else { throw VerificationError.unknownQRVersion(data[data.startIndex]) } let uidLen = Int(data[data.startIndex + 1]) guard data.count >= 2 + uidLen + 32 else { throw VerificationError.qrDataTruncated } let uidData = data[data.startIndex + 2 ..< data.startIndex + 2 + uidLen] guard let userId = String(data: uidData, encoding: .utf8) else { throw VerificationError.invalidUTF8 } let identityKey = Data(data[data.startIndex + 2 + uidLen ..< data.startIndex + 2 + uidLen + 32]) return (userId, identityKey) } // MARK: - Helpers /// Convert up to 8 bytes to UInt64, big-endian. private static func bigEndianUInt64(_ data: Data) -> UInt64 { var result: UInt64 = 0 for byte in data { result = result << 8 | UInt64(byte) } return result } } // MARK: - UInt16 Big-Endian private extension UInt16 { var bigEndianData: Data { var value = self.bigEndian return Data(bytes: &value, count: 2) } } // MARK: - Verification Errors enum VerificationError: Error, LocalizedError { case qrDataTooShort case unknownQRVersion(UInt8) case qrDataTruncated case invalidUTF8 var errorDescription: String? { switch self { case .qrDataTooShort: return "QR data too short" case .unknownQRVersion(let v): return "Unknown QR version: \(v)" case .qrDataTruncated: return "QR data truncated" case .invalidUTF8: return "Invalid UTF-8 in QR data" } } }