TM-SGNL-iOS/SignalServiceKit/Security/OWSFingerprint.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

284 lines
11 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import CryptoKit
public import LibSignalClient
public class OWSFingerprint {
public let myAci: Aci
public let theirAci: Aci
public let myAciIdentityKey: IdentityKey
public let theirAciIdentityKey: IdentityKey
private let myFingerprintData: Data
private let theirFingerprintData: Data
private let hashIterations: UInt32
public let theirName: String
/**
* Formats numeric fingerprint, 3 lines in groups of 5 digits.
*/
public var displayableText: String {
return generateDisplayableText()
}
public var image: UIImage? {
return generateImage()
}
public init(
myAci: Aci,
theirAci: Aci,
myAciIdentityKey: IdentityKey,
theirAciIdentityKey: IdentityKey,
theirName: String,
hashIterations: UInt32 = Constants.defaultHashIterations
) {
self.myAci = myAci
self.theirAci = theirAci
self.myAciIdentityKey = myAciIdentityKey
self.theirAciIdentityKey = theirAciIdentityKey
self.hashIterations = hashIterations
self.theirName = theirName
let (myStableSourceData, theirStableSourceData) = Self.stableData(myAci: myAci, theirAci: theirAci)
self.myFingerprintData = Self.dataForStableAddress(
myStableSourceData,
publicKey: myAciIdentityKey,
hashIterations: hashIterations
)
self.theirFingerprintData = Self.dataForStableAddress(
theirStableSourceData,
publicKey: theirAciIdentityKey,
hashIterations: hashIterations
)
}
public enum MatchResult {
case match
case theyHaveOldVersion(localizedErrorDescription: String)
case weHaveOldVersion(localizedErrorDescription: String)
case noMatch(localizedErrorDescription: String)
}
public func matchesLogicalFingerprintsData(_ otherData: Data) -> MatchResult {
owsAssertDebug(otherData.isEmpty.negated)
let logicalFingerprints: FingerprintProtoLogicalFingerprints
do {
logicalFingerprints = try FingerprintProtoLogicalFingerprints.init(serializedData: otherData)
} catch {
owsFailDebug("fingerprint failure: \(error)")
let description = OWSLocalizedString("PRIVACY_VERIFICATION_FAILURE_INVALID_QRCODE", comment: "alert body")
return .noMatch(localizedErrorDescription: description)
}
if logicalFingerprints.version < self.scannableFingerprintVersion {
Logger.warn("Verification failed. They're running an old version.")
let description = OWSLocalizedString("PRIVACY_VERIFICATION_FAILED_WITH_OLD_REMOTE_VERSION", comment: "alert body")
return .theyHaveOldVersion(localizedErrorDescription: description)
}
if logicalFingerprints.version > self.scannableFingerprintVersion {
Logger.warn("Verification failed. We're running an old version.")
let description = OWSLocalizedString("PRIVACY_VERIFICATION_FAILED_WITH_OLD_LOCAL_VERSION", comment: "alert body")
return .weHaveOldVersion(localizedErrorDescription: description)
}
// Their local is *our* remote.
let localFingerprint = logicalFingerprints.remoteFingerprint
let remoteFingerprint = logicalFingerprints.localFingerprint
if remoteFingerprint.identityData != Self.scannableData(from: self.theirFingerprintData) {
Logger.warn("Verification failed. We have the wrong fingerprint for them")
let descriptionFormat = OWSLocalizedString(
"PRIVACY_VERIFICATION_FAILED_I_HAVE_WRONG_KEY_FOR_THEM",
comment: "Alert body when verifying with {{contact name}}"
)
let description = String(format: descriptionFormat, self.theirName)
return .noMatch(localizedErrorDescription: description)
}
if localFingerprint.identityData != Self.scannableData(from: self.myFingerprintData) {
Logger.warn("Verification failed. They have the wrong fingerprint for us")
let descriptionFormat = OWSLocalizedString(
"PRIVACY_VERIFICATION_FAILED_THEY_HAVE_WRONG_KEY_FOR_ME",
comment: "Alert body when verifying with {{contact name}}"
)
let description = String(format: descriptionFormat, self.theirName)
return .noMatch(localizedErrorDescription: description)
}
Logger.warn("Verification Succeeded.")
return .match
}
// MARK: - Text Representation
private var textRepresentation: String {
let myDisplayString = Self.stringForFingerprintData(myFingerprintData)
let theirDisplayString = Self.stringForFingerprintData(theirFingerprintData)
if theirDisplayString.compare(myDisplayString) == .orderedAscending {
return theirDisplayString + myDisplayString
} else {
return myDisplayString + theirDisplayString
}
}
private func generateDisplayableText() -> String {
let input = self.textRepresentation
var lines = [String]()
let lineLength = (input as NSString).length / 3
for i in 0..<3 {
let line = input.substring(withRange: NSRange(location: i * lineLength, length: lineLength))
var chunks = [String]()
for j in 0..<((line as NSString).length / 5) {
let nextChunk = line.substring(withRange: NSRange(location: j * 5, length: 5))
chunks.append(nextChunk)
}
lines.append(String(chunks.joined(separator: " ")))
}
return String(lines.joined(separator: "\n"))
}
// MARK: - Image Representation
private func generateImage() -> UIImage? {
let remoteFingerprintBuilder = FingerprintProtoLogicalFingerprint.builder(
identityData: Self.scannableData(from: self.theirFingerprintData)
)
let localFingerprintBuilder = FingerprintProtoLogicalFingerprint.builder(
identityData: Self.scannableData(from: self.myFingerprintData)
)
let remoteFingerprint: FingerprintProtoLogicalFingerprint
let localFingerprint: FingerprintProtoLogicalFingerprint
do {
remoteFingerprint = try remoteFingerprintBuilder.build()
localFingerprint = try localFingerprintBuilder.build()
} catch {
owsFailDebug("could not build proto \(error)")
return nil
}
let logicalFingerprintsBuilder = FingerprintProtoLogicalFingerprints.builder(
version: self.scannableFingerprintVersion,
localFingerprint: localFingerprint,
remoteFingerprint: remoteFingerprint
)
let fingerprintData: Data
do {
// Build ByteMode QR (Latin-1 encodable data)
fingerprintData = try logicalFingerprintsBuilder.buildSerializedData()
} catch {
owsFailDebug("could not serialize proto \(error)")
return nil
}
Logger.debug("Building fingerprint")
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {
Logger.error("Failed to create QR code filter")
return nil
}
filter.setDefaults()
filter.setValue(fingerprintData, forKey: "inputMessage")
guard let ciImage = filter.outputImage else {
Logger.error("Failed to create QR image from fingerprint")
return nil
}
// UIImages backed by a CIImage won't render without antialiasing, so we convert the backign image to a CGImage,
// which can be scaled crisply.
let context = CIContext()
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
return nil
}
let qrImage = UIImage(cgImage: cgImage)
return qrImage
}
// MARK: - Private helpers
private static func stableData(myAci: Aci, theirAci: Aci) -> (my: Data, their: Data) {
return (my: myAci.rawUUID.data, their: theirAci.rawUUID.data)
}
/**
* An identifier for a mutable public key, belonging to an immutable identifier (stableId).
*
* This method is intended to be somewhat expensive to produce in order to be brute force adverse.
*
* @param stableAddressData
* Immutable global identifier e.g. Signal Identifier, an e164 formatted phone number encoded as UTF-8 data
* @param publicKey
* The current public key for <stableAddress>
* @return
* All-number textual representation
*/
private static func dataForStableAddress(_ stableAddressData: Data, publicKey: IdentityKey, hashIterations: UInt32) -> Data {
let publicKey = publicKey.serialize().asData
var hash = Constants.hashingVersion.bigEndianData
hash.append(publicKey)
hash.append(stableAddressData)
for _ in 0..<hashIterations {
hash.append(publicKey)
if hash.count >= UInt32.max {
owsFail("Oversize data")
}
let digestData = SHA512.hash(data: hash)
hash.removeAll(keepingCapacity: true)
hash.append(contentsOf: digestData)
}
return hash
}
private static func stringForFingerprintData(_ data: Data) -> String {
return String(
format: "%@%@%@%@%@%@",
encodedChunkFromData(data, offset: 0),
encodedChunkFromData(data, offset: 5),
encodedChunkFromData(data, offset: 10),
encodedChunkFromData(data, offset: 15),
encodedChunkFromData(data, offset: 20),
encodedChunkFromData(data, offset: 25)
)
}
private static func encodedChunkFromData(_ data: Data, offset: Int) -> String {
let fiveByteChunk = Data(data.dropFirst(offset).prefix(5))
let chunk: Int = intFrom5Bytes(fiveByteChunk) % 100000
return String(format: "%05d", chunk)
}
private static func intFrom5Bytes(_ data: Data) -> Int {
return Int(data[0]) << 32
+ Int(data[1]) << 24
+ Int(data[2]) << 16
+ Int(data[3]) << 8
+ Int(data[4])
}
private static func scannableData(from data: Data) -> Data {
return data.prefix(32)
}
private var scannableFingerprintVersion: UInt32 {
return Constants.aciScannableFormatVersion
}
public enum Constants {
static let hashingVersion: UInt16 = 0
static let aciScannableFormatVersion: UInt32 = 2
public static let defaultHashIterations: UInt32 = 5200
}
}