273 lines
11 KiB
Swift
273 lines
11 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
public import GRDB
|
|
public import LibSignalClient
|
|
|
|
private extension OWSVerificationState {
|
|
var protoState: SSKProtoVerifiedState {
|
|
switch self {
|
|
case .default, .defaultAcknowledged:
|
|
return .default
|
|
case .verified:
|
|
return .verified
|
|
case .noLongerVerified:
|
|
return .unverified
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum VerificationState: Equatable {
|
|
case verified
|
|
case noLongerVerified
|
|
case implicit(isAcknowledged: Bool)
|
|
|
|
init(_ verificationState: OWSVerificationState) {
|
|
switch verificationState {
|
|
case .default:
|
|
self = .implicit(isAcknowledged: false)
|
|
case .defaultAcknowledged:
|
|
self = .implicit(isAcknowledged: true)
|
|
case .verified:
|
|
self = .verified
|
|
case .noLongerVerified:
|
|
self = .noLongerVerified
|
|
}
|
|
}
|
|
|
|
var rawValue: OWSVerificationState {
|
|
switch self {
|
|
case .implicit(isAcknowledged: false):
|
|
return .default
|
|
case .implicit(isAcknowledged: true):
|
|
return .defaultAcknowledged
|
|
case .verified:
|
|
return .verified
|
|
case .noLongerVerified:
|
|
return .noLongerVerified
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Record for a recipient's identity key and associated fields used to make trust decisions.
|
|
public final class OWSRecipientIdentity: NSObject, SDSCodableModel, Decodable {
|
|
public static let databaseTableName = "model_OWSRecipientIdentity"
|
|
private static var recordType: SDSRecordType { .recipientIdentity }
|
|
|
|
public var id: Int64?
|
|
public let uniqueId: String
|
|
public let identityKey: Data
|
|
public let createdAt: Date
|
|
public let isFirstKnownKey: Bool
|
|
|
|
public internal(set) var verificationState: OWSVerificationState
|
|
|
|
public init(
|
|
uniqueId: String,
|
|
identityKey: Data,
|
|
isFirstKnownKey: Bool,
|
|
createdAt: Date,
|
|
verificationState: OWSVerificationState,
|
|
) {
|
|
self.uniqueId = uniqueId
|
|
self.identityKey = identityKey
|
|
self.isFirstKnownKey = isFirstKnownKey
|
|
self.createdAt = createdAt
|
|
self.verificationState = verificationState
|
|
}
|
|
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
case id
|
|
case recordType
|
|
case uniqueId
|
|
case accountId
|
|
case identityKey
|
|
case createdAt
|
|
case isFirstKnownKey
|
|
case verificationState
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
let decodedRecordType = try container.decode(Int64.self, forKey: .recordType)
|
|
guard decodedRecordType == Self.recordType.rawValue else {
|
|
owsFailDebug("Unexpected record type: \(decodedRecordType)")
|
|
throw SDSError.invalidValue()
|
|
}
|
|
|
|
self.id = try container.decode(Int64.self, forKey: .id)
|
|
self.uniqueId = try container.decode(String.self, forKey: .uniqueId)
|
|
self.identityKey = try container.decode(Data.self, forKey: .identityKey)
|
|
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
|
self.isFirstKnownKey = try container.decode(Bool.self, forKey: .isFirstKnownKey)
|
|
self.verificationState = OWSVerificationState(rawValue: UInt64(bitPattern: try container.decode(Int64.self, forKey: .verificationState))) ?? .noLongerVerified
|
|
}
|
|
|
|
public func encode(to encoder: any Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encodeIfPresent(self.id, forKey: .id)
|
|
try container.encode(Self.recordType.rawValue, forKey: .recordType)
|
|
try container.encode(self.uniqueId, forKey: .uniqueId)
|
|
try container.encode(self.uniqueId, forKey: .accountId)
|
|
try container.encode(self.identityKey, forKey: .identityKey)
|
|
try container.encode(self.createdAt, forKey: .createdAt)
|
|
try container.encode(self.isFirstKnownKey, forKey: .isFirstKnownKey)
|
|
try container.encode(Int64(bitPattern: self.verificationState.rawValue), forKey: .verificationState)
|
|
}
|
|
|
|
public var wasIdentityVerified: Bool {
|
|
switch self.verificationState {
|
|
case .verified, .noLongerVerified:
|
|
return true
|
|
case .default, .defaultAcknowledged:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var identityKeyObject: IdentityKey {
|
|
get throws {
|
|
try IdentityKey(publicKey: PublicKey(keyData: identityKey))
|
|
}
|
|
}
|
|
|
|
public class func groupContainsUnverifiedMember(
|
|
_ threadUniqueId: String,
|
|
transaction: DBReadTransaction,
|
|
) -> Bool {
|
|
let identityKeys = groupMemberIdentityKeys(
|
|
in: threadUniqueId,
|
|
matching: .verified,
|
|
negated: true,
|
|
limit: 1,
|
|
tx: transaction,
|
|
)
|
|
return !identityKeys.isEmpty
|
|
}
|
|
|
|
public class func noLongerVerifiedIdentityKeys(
|
|
in threadUniqueId: String,
|
|
tx: DBReadTransaction,
|
|
) -> [SignalServiceAddress: Data] {
|
|
return groupMemberIdentityKeys(in: threadUniqueId, matching: .noLongerVerified, negated: false, tx: tx)
|
|
}
|
|
|
|
private class func sqlQueryToFetchIdentityKeys(
|
|
matching verificationState: OWSVerificationState,
|
|
negated: Bool,
|
|
limit: Int,
|
|
) -> String {
|
|
let limitClause: String
|
|
if limit < Int.max {
|
|
limitClause = "LIMIT \(limit)"
|
|
} else {
|
|
limitClause = ""
|
|
}
|
|
|
|
let comparisonOperator = negated ? "!=" : "="
|
|
let recipientIdentity_verificationState = OWSRecipientIdentity.columnName(.verificationState, fullyQualified: true)
|
|
let stateClause = "\(recipientIdentity_verificationState) \(comparisonOperator) \(verificationState.rawValue)"
|
|
|
|
let groupMember_phoneNumber = TSGroupMember.columnName(.phoneNumber, fullyQualified: true)
|
|
let groupMember_groupThreadID = TSGroupMember.columnName(.groupThreadId, fullyQualified: true)
|
|
let groupMember_serviceIdString = TSGroupMember.columnName(.serviceId, fullyQualified: true)
|
|
|
|
let recipient_id = "\(signalRecipientColumnFullyQualified: .id)"
|
|
let recipient_phoneNumber = "\(signalRecipientColumnFullyQualified: .phoneNumber)"
|
|
let recipient_aciString = "\(signalRecipientColumnFullyQualified: .aciString)"
|
|
let recipient_pniString = "\(signalRecipientColumnFullyQualified: .pni)"
|
|
let recipient_uniqueID = "\(signalRecipientColumnFullyQualified: .uniqueId)"
|
|
|
|
let recipientIdentity_uniqueID = OWSRecipientIdentity.columnName(.uniqueId, fullyQualified: true)
|
|
let recipientIdentity_identityKey = OWSRecipientIdentity.columnName(.identityKey, fullyQualified: true)
|
|
let exceptClause = "\(recipient_aciString) != ?"
|
|
let sql =
|
|
"""
|
|
SELECT \(recipient_aciString), \(recipient_phoneNumber), \(recipient_pniString), \(recipientIdentity_identityKey)
|
|
FROM
|
|
\(SignalRecipient.databaseTableName),
|
|
\(OWSRecipientIdentity.databaseTableName),
|
|
\(TSGroupMember.databaseTableName)
|
|
WHERE
|
|
\(recipient_uniqueID) = \(recipientIdentity_uniqueID)
|
|
AND \(groupMember_groupThreadID) = ?
|
|
AND (
|
|
\(groupMember_serviceIdString) = \(recipient_aciString)
|
|
OR \(groupMember_serviceIdString) = \(recipient_pniString)
|
|
OR \(groupMember_phoneNumber) = \(recipient_phoneNumber)
|
|
)
|
|
AND \(exceptClause)
|
|
AND \(stateClause)
|
|
ORDER BY \(recipient_id)
|
|
\(limitClause)
|
|
"""
|
|
return sql
|
|
}
|
|
|
|
private class func groupMemberIdentityKeys(
|
|
in threadUniqueId: String,
|
|
matching verificationState: OWSVerificationState,
|
|
negated: Bool,
|
|
limit: Int = Int.max,
|
|
tx: DBReadTransaction,
|
|
) -> [SignalServiceAddress: Data] {
|
|
// There should always be a recipient UUID, but just in case there isn't provide a fake value that won't
|
|
// affect the results of the query.
|
|
let localRecipientAci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.aci
|
|
let sql = sqlQueryToFetchIdentityKeys(matching: verificationState, negated: negated, limit: limit)
|
|
do {
|
|
let args = [threadUniqueId, localRecipientAci?.serviceIdUppercaseString ?? "fake"]
|
|
let cursor = try Row.fetchCursor(tx.database, sql: sql, arguments: StatementArguments(args))
|
|
var result = [SignalServiceAddress: Data]()
|
|
while let row = try cursor.next() {
|
|
let normalizedAddress = NormalizedDatabaseRecordAddress(
|
|
aci: (row[0] as String?).flatMap { try? Aci.parseFrom(serviceIdString: $0) },
|
|
phoneNumber: row[1],
|
|
pni: (row[2] as String?).flatMap { try? Pni.parseFrom(serviceIdString: $0) },
|
|
)
|
|
let address = SignalServiceAddress(
|
|
serviceId: normalizedAddress?.serviceId,
|
|
phoneNumber: normalizedAddress?.phoneNumber,
|
|
)
|
|
result[address] = row[3]
|
|
}
|
|
return result
|
|
} catch {
|
|
owsFailDebug("error: \(error)")
|
|
return [:]
|
|
}
|
|
}
|
|
|
|
class func buildVerifiedProto(
|
|
destinationAci: Aci,
|
|
identityKey: Data,
|
|
verificationState: OWSVerificationState,
|
|
paddingBytesLength: UInt,
|
|
) -> SSKProtoVerified {
|
|
owsAssertDebug(identityKey.count == OWSIdentityManagerImpl.Constants.identityKeyLength)
|
|
// We only sync users marking as verified. Never sync the conflicted state;
|
|
// the sibling device will figure that out on its own.
|
|
owsAssertDebug(verificationState != .noLongerVerified)
|
|
|
|
let verifiedBuilder = SSKProtoVerified.builder()
|
|
verifiedBuilder.setDestinationAciBinary(destinationAci.serviceIdBinary)
|
|
verifiedBuilder.setIdentityKey(identityKey)
|
|
verifiedBuilder.setState(verificationState.protoState)
|
|
if paddingBytesLength > 0 {
|
|
// We add the same amount of padding in the VerificationStateSync message
|
|
// and its corresponding NullMessage so that the sync message is
|
|
// indistinguishable from an outgoing Sent transcript corresponding to the
|
|
// NullMessage. We pad the NullMessage so as to obscure its content. The
|
|
// sync message (like all sync messages) will be *additionally* padded by
|
|
// the superclass while being sent. The end result is we send a NullMessage
|
|
// of a non-distinct size, and a verification sync which is ~1-512 bytes
|
|
// larger than that.
|
|
verifiedBuilder.setNullMessage(Randomness.generateRandomBytes(paddingBytesLength))
|
|
}
|
|
return verifiedBuilder.buildInfallibly()
|
|
}
|
|
}
|