ios_client
This commit is contained in:
162
ios_client 0.8.5/Kecalek/Crypto/ContactVerification.swift
Normal file
162
ios_client 0.8.5/Kecalek/Crypto/ContactVerification.swift
Normal file
@@ -0,0 +1,162 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user