Files
Kecalek_python/ios_client 0.8.5/Kecalek/Crypto/ContactVerification.swift
2026-03-14 12:43:56 +01:00

163 lines
5.8 KiB
Swift

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..<iterations {
var hasher = SHA512()
hasher.update(data: data)
hasher.update(data: identityKey)
let digest = hasher.finalize()
data = Data(digest)
}
return Data(data.prefix(32))
}
/// Format 32-byte fingerprint as 6 groups of 5 zero-padded digits (30 digits).
///
/// Each group: int(bytes[i*5:(i+1)*5], big-endian) % 100000.
/// Output: two lines of 3 groups each, space-separated.
static func formatFingerprint(_ fpBytes: Data) -> 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"
}
}
}