Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Security/OWSRecipientIdentity.swift
1 views
//
// 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()
    }
}