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

1092 lines
46 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import CryptoKit
public import LibSignalClient
public enum IdentityManagerError: Error, IsRetryableProvider {
case identityKeyMismatchForOutgoingMessage
public var isRetryableProvider: Bool { false }
}
public protocol OWSIdentityManager {
func libSignalStore(for identity: OWSIdentity, tx: DBReadTransaction) throws -> IdentityStore
func groupContainsUnverifiedMember(_ groupUniqueID: String, tx: DBReadTransaction) -> Bool
func fireIdentityStateChangeNotification(after tx: DBWriteTransaction)
func recipientIdentity(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSRecipientIdentity?
func recipientIdentity(for recipientUniqueId: RecipientUniqueId, tx: DBReadTransaction) -> OWSRecipientIdentity?
func removeRecipientIdentity(for recipientUniqueId: RecipientUniqueId, tx: DBWriteTransaction)
func identityKeyPair(for identity: OWSIdentity, tx: DBReadTransaction) -> ECKeyPair?
func setIdentityKeyPair(_ keyPair: ECKeyPair?, for identity: OWSIdentity, tx: DBWriteTransaction)
func identityKey(for address: SignalServiceAddress, tx: DBReadTransaction) -> Data?
func identityKey(for serviceId: ServiceId, tx: DBReadTransaction) throws -> IdentityKey?
@discardableResult
func saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError>
func insertIdentityChangeInfoMessage(for serviceId: ServiceId, wasIdentityVerified: Bool, tx: DBWriteTransaction)
func insertSessionSwitchoverEvent(for recipient: SignalRecipient, phoneNumber: String?, tx: DBWriteTransaction)
func mergeRecipient(_ recipient: SignalRecipient, into targetRecipient: SignalRecipient, tx: DBWriteTransaction)
func untrustedIdentityForSending(
to address: SignalServiceAddress,
untrustedThreshold: Date?,
tx: DBReadTransaction
) -> OWSRecipientIdentity?
func tryToSyncQueuedVerificationStates()
func verificationState(for address: SignalServiceAddress, tx: DBReadTransaction) -> VerificationState
func setVerificationState(
_ verificationState: VerificationState,
of identityKey: Data,
for address: SignalServiceAddress,
isUserInitiatedChange: Bool,
tx: DBWriteTransaction
) -> ChangeVerificationStateResult
func processIncomingVerifiedProto(_ verified: SSKProtoVerified, tx: DBWriteTransaction) throws
func shouldSharePhoneNumber(with serviceId: ServiceId, tx: DBReadTransaction) -> Bool
func setShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction)
func clearShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction)
func clearShouldSharePhoneNumberForEveryone(tx: DBWriteTransaction)
func batchUpdateIdentityKeys(for serviceIds: [ServiceId]) -> Promise<Void>
}
extension OWSIdentityManager {
@discardableResult
public func saveIdentityKey(_ identityKey: IdentityKey, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError> {
return saveIdentityKey(identityKey.publicKey.keyBytes.asData, for: serviceId, tx: tx)
}
}
public enum TSMessageDirection {
case incoming
case outgoing
}
public enum ChangeVerificationStateResult {
case error
case redundant
case success
}
private extension TSMessageDirection {
init(_ direction: Direction) {
switch direction {
case .receiving:
self = .incoming
case .sending:
self = .outgoing
}
}
}
extension OWSIdentity: CustomStringConvertible {
public var description: String {
switch self {
case .aci:
return "ACI"
case .pni:
return "PNI"
}
}
}
public class IdentityStore: IdentityKeyStore {
private let identityManager: OWSIdentityManagerImpl
private let identityKeyPair: IdentityKeyPair
private let fetchLocalRegistrationId: (DBWriteTransaction) -> UInt32
fileprivate init(
identityManager: OWSIdentityManagerImpl,
identityKeyPair: IdentityKeyPair,
fetchLocalRegistrationId: @escaping (DBWriteTransaction) -> UInt32
) {
self.identityManager = identityManager
self.identityKeyPair = identityKeyPair
self.fetchLocalRegistrationId = fetchLocalRegistrationId
}
public func identityKeyPair(context: StoreContext) throws -> IdentityKeyPair {
return identityKeyPair
}
public func localRegistrationId(context: StoreContext) throws -> UInt32 {
return fetchLocalRegistrationId(context.asTransaction.asV2Write)
}
public func saveIdentity(
_ identityKey: IdentityKey,
for address: ProtocolAddress,
context: StoreContext
) throws -> Bool {
try identityManager.saveIdentityKey(
identityKey,
for: address.serviceId,
tx: context.asTransaction.asV2Write
).get()
}
public func isTrustedIdentity(
_ identityKey: IdentityKey,
for address: ProtocolAddress,
direction: Direction,
context: StoreContext
) throws -> Bool {
return try identityManager.isTrustedIdentityKey(
identityKey,
serviceId: address.serviceId,
direction: TSMessageDirection(direction),
tx: context.asTransaction.asV2Read
)
}
public func identity(for address: ProtocolAddress, context: StoreContext) throws -> LibSignalClient.IdentityKey? {
return try identityManager.identityKey(for: address.serviceId, tx: context.asTransaction.asV2Read)
}
}
extension NSNotification.Name {
// This notification will be fired whenever identities are created
// or their verification state changes.
public static let identityStateDidChange = Notification.Name("kNSNotificationNameIdentityStateDidChange")
}
extension OWSIdentityManagerImpl {
public enum Constants {
// The canonical key includes 32 bytes of identity material plus one byte specifying the key type
static let identityKeyLength = 33
// Cryptographic operations do not use the "type" byte of the identity key, so, for legacy reasons we store just
// the identity material.
fileprivate static let storedIdentityKeyLength = 32
/// Don't trust an identity for sending to unless they've been around for at least this long.
public static let defaultUntrustedInterval: TimeInterval = 5
}
}
private extension OWSIdentity {
var persistenceKey: String {
switch self {
case .aci:
return "TSStorageManagerIdentityKeyStoreIdentityKey"
case .pni:
return "TSStorageManagerIdentityKeyStorePNIIdentityKey"
}
}
}
extension OWSIdentityManager {
func generateNewIdentityKeyPair() -> ECKeyPair {
ECKeyPair.generateKeyPair()
}
}
public class OWSIdentityManagerImpl: OWSIdentityManager {
private let aciProtocolStore: SignalProtocolStore
private let appReadiness: AppReadiness
private let db: any DB
private let messageSenderJobQueue: MessageSenderJobQueue
private let networkManager: NetworkManager
private let notificationPresenter: any NotificationPresenter
private let ownIdentityKeyValueStore: KeyValueStore
private let pniProtocolStore: SignalProtocolStore
private let queuedVerificationStateSyncMessagesKeyValueStore: KeyValueStore
private let recipientFetcher: RecipientFetcher
private let recipientIdFinder: RecipientIdFinder
private let schedulers: Schedulers
private let shareMyPhoneNumberStore: KeyValueStore
private let storageServiceManager: StorageServiceManager
private let tsAccountManager: TSAccountManager
public init(
aciProtocolStore: SignalProtocolStore,
appReadiness: AppReadiness,
db: any DB,
messageSenderJobQueue: MessageSenderJobQueue,
networkManager: NetworkManager,
notificationPresenter: any NotificationPresenter,
pniProtocolStore: SignalProtocolStore,
recipientFetcher: RecipientFetcher,
recipientIdFinder: RecipientIdFinder,
schedulers: Schedulers,
storageServiceManager: StorageServiceManager,
tsAccountManager: TSAccountManager
) {
self.aciProtocolStore = aciProtocolStore
self.appReadiness = appReadiness
self.db = db
self.messageSenderJobQueue = messageSenderJobQueue
self.networkManager = networkManager
self.notificationPresenter = notificationPresenter
self.ownIdentityKeyValueStore = KeyValueStore(
collection: "TSStorageManagerIdentityKeyStoreCollection"
)
self.pniProtocolStore = pniProtocolStore
self.queuedVerificationStateSyncMessagesKeyValueStore = KeyValueStore(
collection: "OWSIdentityManager_QueuedVerificationStateSyncMessages"
)
self.recipientFetcher = recipientFetcher
self.recipientIdFinder = recipientIdFinder
self.schedulers = schedulers
self.shareMyPhoneNumberStore = KeyValueStore(
collection: "OWSIdentityManager.shareMyPhoneNumberStore"
)
self.storageServiceManager = storageServiceManager
self.tsAccountManager = tsAccountManager
SwiftSingletons.register(self)
}
public func libSignalStore(for identity: OWSIdentity, tx: DBReadTransaction) throws -> IdentityStore {
guard let identityKeyPair = self.identityKeyPair(for: identity, tx: tx) else {
throw OWSAssertionError("no identity key pair for \(identity)")
}
return IdentityStore(
identityManager: self,
identityKeyPair: identityKeyPair.identityKeyPair,
fetchLocalRegistrationId: { [tsAccountManager] in
switch identity {
case .aci:
return tsAccountManager.getOrGenerateAciRegistrationId(tx: $0)
case .pni:
return tsAccountManager.getOrGeneratePniRegistrationId(tx: $0)
}
}
)
}
public func groupContainsUnverifiedMember(_ groupUniqueID: String, tx: DBReadTransaction) -> Bool {
return OWSRecipientIdentity.groupContainsUnverifiedMember(groupUniqueID, transaction: SDSDB.shimOnlyBridge(tx))
}
public func fireIdentityStateChangeNotification(after tx: DBWriteTransaction) {
tx.addAsyncCompletion(on: schedulers.main) {
NotificationCenter.default.post(name: .identityStateDidChange, object: nil)
}
}
// MARK: - Fetching
public func recipientIdentity(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSRecipientIdentity? {
guard let recipientIdResult = recipientIdFinder.recipientUniqueId(for: address, tx: tx) else {
return nil
}
switch recipientIdResult {
case .failure(.mustNotUsePniBecauseAciExists):
// If we pretend as though this identity doesn't exist, we'll get an error
// when we try to send a message, we'll retry, and then we'll correctly
// send to the ACI.
return nil
case .success(let recipientUniqueId):
return recipientIdentity(for: recipientUniqueId, tx: tx)
}
}
public func recipientIdentity(for recipientUniqueId: RecipientUniqueId, tx: DBReadTransaction) -> OWSRecipientIdentity? {
return OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
}
public func removeRecipientIdentity(for recipientUniqueId: RecipientUniqueId, tx: DBWriteTransaction) {
recipientIdentity(for: recipientUniqueId, tx: tx)?.anyRemove(transaction: SDSDB.shimOnlyBridge(tx))
}
// MARK: - Local Identity
public func identityKeyPair(for identity: OWSIdentity, tx: DBReadTransaction) -> ECKeyPair? {
return ownIdentityKeyValueStore.getObject(identity.persistenceKey, ofClass: ECKeyPair.self, transaction: tx)
}
public func setIdentityKeyPair(_ keyPair: ECKeyPair?, for identity: OWSIdentity, tx: DBWriteTransaction) {
// Under no circumstances may we *clear* our *ACI* identity key.
owsPrecondition(keyPair != nil || identity != .aci)
ownIdentityKeyValueStore.setObject(keyPair, key: identity.persistenceKey, transaction: tx)
}
// MARK: - Remote Identity Keys
public func identityKey(for address: SignalServiceAddress, tx: DBReadTransaction) -> Data? {
switch recipientIdFinder.recipientUniqueId(for: address, tx: tx) {
case .none, .some(.failure(.mustNotUsePniBecauseAciExists)):
return nil
case .some(.success(let recipientUniqueId)):
return _identityKey(for: recipientUniqueId, tx: tx)
}
}
public func identityKey(for serviceId: ServiceId, tx: DBReadTransaction) throws -> IdentityKey? {
guard let recipientIdResult = recipientIdFinder.recipientUniqueId(for: serviceId, tx: tx) else {
return nil
}
guard let keyData = try _identityKey(for: recipientIdResult.get(), tx: tx) else { return nil }
return try IdentityKey(publicKey: PublicKey(keyData: keyData))
}
private func _identityKey(for recipientUniqueId: RecipientUniqueId, tx: DBReadTransaction) -> Data? {
owsAssertDebug(!recipientUniqueId.isEmpty)
return OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))?.identityKey
}
@discardableResult
public func saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError> {
let recipientIdResult = recipientIdFinder.ensureRecipientUniqueId(for: serviceId, tx: tx)
return recipientIdResult.map({ _saveIdentityKey(identityKey, for: serviceId, recipientUniqueId: $0, tx: tx) })
}
private func _saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, recipientUniqueId: RecipientUniqueId, tx: DBWriteTransaction) -> Bool {
owsAssertDebug(identityKey.count == Constants.storedIdentityKeyLength)
let existingIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
guard let existingIdentity else {
Logger.info("Saving first-use identity for \(serviceId)")
OWSRecipientIdentity(
recipientUniqueId: recipientUniqueId,
identityKey: identityKey,
isFirstKnownKey: true,
createdAt: Date(),
verificationState: .default
).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
// Cancel any pending verification state sync messages for this recipient.
clearSyncMessage(for: recipientUniqueId, tx: tx)
fireIdentityStateChangeNotification(after: tx)
storageServiceManager.recordPendingUpdates(updatedRecipientUniqueIds: [recipientUniqueId])
return false
}
guard existingIdentity.identityKey != identityKey else {
return false
}
let verificationState: VerificationState
switch VerificationState(existingIdentity.verificationState) {
case .implicit(isAcknowledged: _):
verificationState = .implicit(isAcknowledged: false)
case .verified, .noLongerVerified:
verificationState = .noLongerVerified
}
Logger.info("Saving new identity for \(serviceId): \(existingIdentity.verificationState) -> \(verificationState)")
insertIdentityChangeInfoMessage(for: serviceId, wasIdentityVerified: existingIdentity.wasIdentityVerified, tx: tx)
OWSRecipientIdentity(
recipientUniqueId: recipientUniqueId,
identityKey: identityKey,
isFirstKnownKey: false,
createdAt: Date(),
verificationState: verificationState.rawValue
).anyUpsert(transaction: SDSDB.shimOnlyBridge(tx))
aciProtocolStore.sessionStore.archiveAllSessions(for: serviceId, tx: tx)
// Cancel any pending verification state sync messages for this recipient.
clearSyncMessage(for: recipientUniqueId, tx: tx)
storageServiceManager.recordPendingUpdates(updatedRecipientUniqueIds: [recipientUniqueId])
return true
}
public func insertIdentityChangeInfoMessage(
for serviceId: ServiceId,
wasIdentityVerified: Bool,
tx: DBWriteTransaction
) {
let contactThread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(serviceId),
transaction: SDSDB.shimOnlyBridge(tx)
)
let contactThreadMessage: TSErrorMessage = .nonblockingIdentityChange(
thread: contactThread,
address: SignalServiceAddress(serviceId),
wasIdentityVerified: wasIdentityVerified
)
contactThreadMessage.anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
for groupThread in TSGroupThread.groupThreads(with: SignalServiceAddress(serviceId), transaction: SDSDB.shimOnlyBridge(tx)) {
TSErrorMessage.nonblockingIdentityChange(
thread: groupThread,
address: SignalServiceAddress(serviceId),
wasIdentityVerified: wasIdentityVerified
).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
}
notificationPresenter.notifyUser(forErrorMessage: contactThreadMessage, thread: contactThread, transaction: SDSDB.shimOnlyBridge(tx))
fireIdentityStateChangeNotification(after: tx)
}
public func insertSessionSwitchoverEvent(
for recipient: SignalRecipient,
phoneNumber: String?,
tx: DBWriteTransaction
) {
let tx = SDSDB.shimOnlyBridge(tx)
guard let contactThread = TSContactThread.getWithContactAddress(recipient.address, transaction: tx) else {
return
}
let sessionSwitchoverEvent: TSInfoMessage = .makeForSessionSwitchover(
contactThread: contactThread,
phoneNumber: phoneNumber
)
sessionSwitchoverEvent.anyInsert(transaction: tx)
}
public func mergeRecipient(_ recipient: SignalRecipient, into targetRecipient: SignalRecipient, tx: DBWriteTransaction) {
let recipientPair = MergePair(fromValue: recipient, intoValue: targetRecipient)
let recipientIdentity = recipientPair.map {
OWSRecipientIdentity.anyFetch(uniqueId: $0.uniqueId, transaction: SDSDB.shimOnlyBridge(tx))
}
guard let fromValue = recipientIdentity.fromValue else {
return
}
if recipientIdentity.intoValue == nil {
OWSRecipientIdentity(
recipientUniqueId: targetRecipient.uniqueId,
identityKey: fromValue.identityKey,
isFirstKnownKey: fromValue.isFirstKnownKey,
createdAt: fromValue.createdAt,
verificationState: fromValue.verificationState
).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
}
fromValue.anyRemove(transaction: SDSDB.shimOnlyBridge(tx))
}
// MARK: - Trust
public func untrustedIdentityForSending(
to address: SignalServiceAddress,
untrustedThreshold: Date?,
tx: DBReadTransaction
) -> OWSRecipientIdentity? {
let recipientIdentity = recipientIdentity(for: address, tx: tx)
let isTrusted = isIdentityKeyTrustedForSending(
address: address,
recipientIdentity: recipientIdentity,
untrustedThreshold: untrustedThreshold,
tx: tx
)
return isTrusted ? nil : recipientIdentity
}
private func isIdentityKeyTrustedForSending(
address: SignalServiceAddress,
recipientIdentity: OWSRecipientIdentity?,
untrustedThreshold: Date?,
tx: DBReadTransaction
) -> Bool {
owsAssertDebug(address.isValid)
if address.isLocalAddress {
guard let recipientIdentity else {
// Trust on first use.
return true
}
return isTrustedLocalKey(recipientIdentity.identityKey, tx: tx)
}
return canSend(to: recipientIdentity, untrustedThreshold: untrustedThreshold)
}
func isTrustedIdentityKey(
_ identityKey: IdentityKey,
serviceId: ServiceId,
direction: TSMessageDirection,
tx: DBReadTransaction
) throws -> Bool {
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx)
if localIdentifiers?.aci == serviceId {
return isTrustedLocalKey(identityKey.publicKey.keyBytes.asData, tx: tx)
}
switch direction {
case .incoming:
return true
case .outgoing:
guard let recipientUniqueId = try recipientIdFinder.recipientUniqueId(for: serviceId, tx: tx)?.get() else {
owsFailDebug("Couldn't find recipientUniqueId for outgoing message.")
return false
}
let recipientIdentity = OWSRecipientIdentity.anyFetch(
uniqueId: recipientUniqueId,
transaction: SDSDB.shimOnlyBridge(tx)
)
if let recipientIdentity, recipientIdentity.identityKey != identityKey.publicKey.keyBytes.asData {
Logger.warn("Key mismatch for \(serviceId)")
throw IdentityManagerError.identityKeyMismatchForOutgoingMessage
}
return canSend(to: recipientIdentity, untrustedThreshold: nil)
}
}
private func isTrustedLocalKey(_ identityKey: Data, tx: DBReadTransaction) -> Bool {
let localIdentityKeyPair = identityKeyPair(for: .aci, tx: tx)
guard localIdentityKeyPair?.publicKey == identityKey else {
owsFailDebug("Wrong identity key for local account.")
return false
}
return true
}
private func canSend(to recipientIdentity: OWSRecipientIdentity?, untrustedThreshold: Date?) -> Bool {
guard let recipientIdentity else {
// Trust on first use.
return true
}
if recipientIdentity.isFirstKnownKey {
return true
}
switch recipientIdentity.verificationState {
case .default:
// This user has never been explicitly verified, but we still want to check
// if the identity key is one we newly learned about to give the local user
// time to ensure they wish to send. If it has been created in the last N
// seconds, we'll treat it as untrusted so sends fail. This is a best
// effort, and we'll continue to allow sending to the user after the "new"
// window elapses without any explicit action from the local user.
let untrustedThreshold = untrustedThreshold ?? Date(timeIntervalSinceNow: -Constants.defaultUntrustedInterval)
guard recipientIdentity.createdAt <= untrustedThreshold else {
Logger.warn("Not trusting new identity for \(recipientIdentity.accountId)")
return false
}
return true
case .defaultAcknowledged:
return true
case .verified:
return true
case .noLongerVerified:
// This user was previously verified and their key has changed. We will not trust
// them again until the user explicitly acknowledges the key change.
Logger.warn("Not trusting no-longer-verified identity for \(recipientIdentity.accountId)")
return false
}
}
// MARK: - Sync Messages
private func enqueueSyncMessage(for recipientUniqueId: RecipientUniqueId, tx: DBWriteTransaction) {
queuedVerificationStateSyncMessagesKeyValueStore.setObject(true, key: recipientUniqueId, transaction: tx)
schedulers.main.async { self.tryToSyncQueuedVerificationStates() }
}
private func clearSyncMessage(for key: String, tx: DBWriteTransaction) {
queuedVerificationStateSyncMessagesKeyValueStore.setObject(nil, key: key, transaction: tx)
}
public func tryToSyncQueuedVerificationStates() {
AssertIsOnMainThread()
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
self.schedulers.global().async { self.syncQueuedVerificationStates() }
}
}
private func syncQueuedVerificationStates() {
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
return
}
guard let thread = TSContactThread.getOrCreateLocalThreadWithSneakyTransaction() else {
owsFailDebug("Missing thread.")
return
}
let allKeys = db.read { tx in queuedVerificationStateSyncMessagesKeyValueStore.allKeys(transaction: tx) }
// We expect very few keys in practice, and each key triggers multiple
// database write transactions. If we do end up with thousands of keys,
// using a separate transaction avoids long blocks.
for key in allKeys {
let syncMessage = db.write { (tx) -> OWSVerificationStateSyncMessage? in
guard let syncMessage = buildVerificationStateSyncMessage(for: key, localThread: thread, tx: tx) else {
queuedVerificationStateSyncMessagesKeyValueStore.removeValue(forKey: key, transaction: tx)
return nil
}
return syncMessage
}
guard let syncMessage else {
continue
}
sendVerificationStateSyncMessage(for: key, message: syncMessage)
}
}
private func buildVerificationStateSyncMessage(
for key: String,
localThread: TSThread,
tx: DBReadTransaction
) -> OWSVerificationStateSyncMessage? {
let value: Any? = queuedVerificationStateSyncMessagesKeyValueStore.getObject(
key,
ofClasses: [NSNumber.self, NSString.self, SignalServiceAddress.self],
transaction: tx
)
guard let value else {
return nil
}
let recipientUniqueId: RecipientUniqueId
switch value {
case let numberValue as NSNumber:
guard numberValue.boolValue else {
return nil
}
recipientUniqueId = key
case is SignalServiceAddress:
recipientUniqueId = key
case let stringValue as NSString:
// Previously, we stored phone numbers in this KV store.
let address = SignalServiceAddress.legacyAddress(serviceId: nil, phoneNumber: stringValue as String)
guard let recipientUniqueId_ = try? recipientIdFinder.recipientUniqueId(for: address, tx: tx)?.get() else {
return nil
}
recipientUniqueId = recipientUniqueId_
default:
return nil
}
if recipientUniqueId.isEmpty {
return nil
}
let recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
guard let recipientIdentity else {
owsFailDebug("Couldn't load recipient identity for \(recipientUniqueId)")
return nil
}
guard let identityKey = try? recipientIdentity.identityKeyObject else {
owsFailDebug("Invalid recipient identity key for \(recipientUniqueId)")
return nil
}
// We don't want to sync "no longer verified" state. Other
// clients can figure this out from the /profile/ endpoint, and
// this can cause data loss as a user's devices overwrite each
// other's verification.
if recipientIdentity.verificationState == .noLongerVerified {
owsFailDebug("Queue verification state is invalid for \(recipientUniqueId)")
return nil
}
guard let recipient = SignalRecipient.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx)) else {
return nil
}
return OWSVerificationStateSyncMessage(
thread: localThread,
verificationState: recipientIdentity.verificationState,
identityKey: identityKey.serialize().asData,
verificationForRecipientAddress: recipient.address,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
private func sendVerificationStateSyncMessage(for recipientUniqueId: RecipientUniqueId, message: OWSVerificationStateSyncMessage) {
let address = message.verificationForRecipientAddress
let contactThread = TSContactThread.getOrCreateThread(contactAddress: address)
// DURABLE CLEANUP - we could replace the custom durability logic in this class
// with a durable JobQueue.
let nullMessagePromise = db.write { tx in
// Send null message to appear as though we're sending a normal message to cover the sync message sent
// subsequently
let nullMessage = OWSOutgoingNullMessage(
contactThread: contactThread,
verificationStateSyncMessage: message,
transaction: SDSDB.shimOnlyBridge(tx)
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: nullMessage
)
return messageSenderJobQueue.add(
.promise,
message: preparedMessage,
limitToCurrentProcessLifetime: true,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
nullMessagePromise.done(on: schedulers.global()) {
Logger.info("Successfully sent verification state NullMessage")
let syncMessagePromise = self.db.write { tx in
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: message
)
return self.messageSenderJobQueue.add(
.promise,
message: preparedMessage,
limitToCurrentProcessLifetime: true,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
syncMessagePromise.done(on: self.schedulers.global()) {
Logger.info("Successfully sent verification state sync message")
self.db.write { tx in self.clearSyncMessage(for: recipientUniqueId, tx: tx) }
}.catch(on: self.schedulers.global()) { error in
Logger.error("Failed to send verification state sync message: \(error)")
}
}.catch(on: schedulers.global()) { error in
Logger.error("Failed to send verification state NullMessage: \(error)")
if error is MessageSenderNoSuchSignalRecipientError {
Logger.info("Removing retries for syncing verification for unregistered user: \(address)")
self.db.write { tx in self.clearSyncMessage(for: recipientUniqueId, tx: tx) }
}
}
}
// MARK: - Verification
public func verificationState(for address: SignalServiceAddress, tx: DBReadTransaction) -> VerificationState {
return VerificationState(recipientIdentity(for: address, tx: tx)?.verificationState ?? .default)
}
public func setVerificationState(
_ verificationState: VerificationState,
of identityKey: Data,
for address: SignalServiceAddress,
isUserInitiatedChange: Bool,
tx: DBWriteTransaction
) -> ChangeVerificationStateResult {
owsAssertDebug(identityKey.count == Constants.storedIdentityKeyLength)
let recipient = OWSAccountIdFinder.ensureRecipient(forAddress: address, transaction: SDSDB.shimOnlyBridge(tx))
let recipientUniqueId = recipient.uniqueId
let recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
guard let recipientIdentity else {
owsFailDebug("Missing OWSRecipientIdentity.")
return .error
}
guard recipientIdentity.identityKey == identityKey else {
Logger.warn("Can't change verification state for outdated identity key")
return .error
}
let oldVerificationState = VerificationState(recipientIdentity.verificationState)
if oldVerificationState == verificationState {
return .redundant
}
// If we're sending to a Pni, the identity key might change. If that
// happens, we can acknowledge it, but we can't mark it as verified.
if recipient.pni != nil, recipient.aciString == nil {
switch verificationState {
case .verified, .noLongerVerified, .implicit(isAcknowledged: false):
owsFailDebug("Can't mark Pni recipient as verified/no longer verified.")
return .error
case .implicit(isAcknowledged: true):
break
}
}
Logger.info("setVerificationState for \(recipientUniqueId): \(recipientIdentity.verificationState) -> \(verificationState)")
recipientIdentity.update(with: verificationState.rawValue, transaction: SDSDB.shimOnlyBridge(tx))
switch (oldVerificationState, verificationState) {
case (.implicit, .implicit):
// We're only changing `isAcknowledged`, and that doesn't impact Storage
// Service, sync messages, or chat events.
break
default:
if isUserInitiatedChange {
saveChangeMessages(for: recipient, verificationState: verificationState, isLocalChange: true, tx: tx)
enqueueSyncMessage(for: recipientUniqueId, tx: tx)
} else {
// Cancel any pending verification state sync messages for this recipient.
clearSyncMessage(for: recipientUniqueId, tx: tx)
}
// Verification state has changed, so notify storage service.
storageServiceManager.recordPendingUpdates(updatedRecipientUniqueIds: [recipientUniqueId])
}
fireIdentityStateChangeNotification(after: tx)
return .success
}
// MARK: - Verified
public func processIncomingVerifiedProto(_ verified: SSKProtoVerified, tx: DBWriteTransaction) throws {
guard let aci = Aci.parseFrom(aciString: verified.destinationAci) else {
return owsFailDebug("Verification state sync message missing destination.")
}
Logger.info("Received verification state message for \(aci)")
guard let rawIdentityKey = verified.identityKey else {
return owsFailDebug("Verification state sync message for \(aci) with malformed identityKey")
}
let identityKey = try IdentityKey(bytes: rawIdentityKey)
switch verified.state {
case .default:
applyVerificationStateAction(
.clearVerification,
aci: aci,
identityKey: identityKey,
overwriteOnConflict: false,
tx: tx
)
case .verified:
applyVerificationStateAction(
.markVerified,
aci: aci,
identityKey: identityKey,
overwriteOnConflict: true,
tx: tx
)
case .unverified:
return owsFailDebug("Verification state sync message for \(aci) has unverified state")
case .none:
return owsFailDebug("Verification state sync message for \(aci) has no state")
}
}
private enum VerificationStateAction {
case markVerified
case clearVerification
}
private func applyVerificationStateAction(
_ verificationStateAction: VerificationStateAction,
aci: Aci,
identityKey: IdentityKey,
overwriteOnConflict: Bool,
tx: DBWriteTransaction
) {
let recipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
let recipientUniqueId = recipient.uniqueId
var recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
let shouldSaveIdentityKey: Bool
let shouldInsertChangeMessages: Bool
if let recipientIdentity {
if recipientIdentity.accountId != recipientUniqueId {
return owsFailDebug("Unexpected recipientUniqueId for \(aci)")
}
let didChangeIdentityKey = recipientIdentity.identityKey != identityKey.publicKey.keyBytes.asData
if didChangeIdentityKey, !overwriteOnConflict {
// The conflict case where we receive a verification sync message whose
// identity key disagrees with the local identity key for this recipient.
Logger.warn("Non-matching identityKey for \(aci)")
return
}
shouldSaveIdentityKey = didChangeIdentityKey
shouldInsertChangeMessages = true
} else if verificationStateAction == .clearVerification {
// There's no point in creating a new recipient identity just to set its
// verification state to default.
return
} else {
shouldSaveIdentityKey = true
shouldInsertChangeMessages = false
}
if shouldSaveIdentityKey {
// Ensure a remote identity exists for this key. We may be learning about
// it for the first time.
saveIdentityKey(identityKey, for: aci, tx: tx)
recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientUniqueId, transaction: SDSDB.shimOnlyBridge(tx))
}
guard let recipientIdentity else {
return owsFailDebug("Missing expected identity for \(aci)")
}
guard recipientIdentity.accountId == recipientUniqueId else {
return owsFailDebug("Unexpected recipientUniqueId for \(aci)")
}
guard recipientIdentity.identityKey == identityKey.publicKey.keyBytes.asData else {
return owsFailDebug("Unexpected identityKey for \(aci)")
}
let oldVerificationState: VerificationState = VerificationState(recipientIdentity.verificationState)
let newVerificationState: VerificationState
switch verificationStateAction {
case .markVerified:
switch oldVerificationState {
case .verified:
return
case .noLongerVerified, .implicit(isAcknowledged: _):
newVerificationState = .verified
}
case .clearVerification:
switch oldVerificationState {
case .implicit:
return // We can keep any implicit state.
case .verified, .noLongerVerified:
newVerificationState = .implicit(isAcknowledged: false)
}
}
Logger.info("for \(aci): \(oldVerificationState) -> \(newVerificationState)")
recipientIdentity.update(with: newVerificationState.rawValue, transaction: SDSDB.shimOnlyBridge(tx))
if shouldInsertChangeMessages {
saveChangeMessages(for: recipient, verificationState: newVerificationState, isLocalChange: false, tx: tx)
}
}
private func saveChangeMessages(
for signalRecipient: SignalRecipient,
verificationState: VerificationState,
isLocalChange: Bool,
tx: DBWriteTransaction
) {
let address = signalRecipient.address
var relevantThreads = [TSThread]()
relevantThreads.append(TSContactThread.getOrCreateThread(withContactAddress: address, transaction: SDSDB.shimOnlyBridge(tx)))
relevantThreads.append(contentsOf: TSGroupThread.groupThreads(with: address, transaction: SDSDB.shimOnlyBridge(tx)))
for thread in relevantThreads {
OWSVerificationStateChangeMessage(
thread: thread,
timestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(),
recipientAddress: address,
verificationState: verificationState.rawValue,
isLocalChange: isLocalChange
).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
}
}
// MARK: - Phone Number Sharing
public func shouldSharePhoneNumber(with recipient: ServiceId, tx: DBReadTransaction) -> Bool {
guard let recipient = recipient as? Aci else {
return false
}
let aciString = recipient.serviceIdUppercaseString
return shareMyPhoneNumberStore.getBool(aciString, defaultValue: false, transaction: tx)
}
public func setShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction) {
let aciString = recipient.serviceIdUppercaseString
shareMyPhoneNumberStore.setBool(true, key: aciString, transaction: tx)
}
public func clearShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction) {
let aciString = recipient.serviceIdUppercaseString
shareMyPhoneNumberStore.removeValue(forKey: aciString, transaction: tx)
}
public func clearShouldSharePhoneNumberForEveryone(tx: DBWriteTransaction) {
shareMyPhoneNumberStore.removeAll(transaction: tx)
}
// MARK: - Batch Identity Lookup
public func batchUpdateIdentityKeys(for serviceIds: [ServiceId]) -> Promise<Void> {
if serviceIds.isEmpty { return .value(()) }
let serviceIds = Set(serviceIds)
let batchServiceIds = serviceIds.prefix(OWSRequestFactory.batchIdentityCheckElementsLimit)
let remainingServiceIds = Array(serviceIds.subtracting(batchServiceIds))
return firstly(on: schedulers.global()) { () -> Promise<HTTPResponse> in
Logger.info("Performing batch identity key lookup for \(batchServiceIds.count) addresses. \(remainingServiceIds.count) remaining.")
let elements = self.db.read { tx in
batchServiceIds.compactMap { serviceId -> [String: String]? in
guard let identityKey = try? self.identityKey(for: serviceId, tx: tx) else { return nil }
let externalIdentityKey = identityKey.serialize().asData
let identityKeyDigest = Data(SHA256.hash(data: externalIdentityKey))
return ["uuid": serviceId.serviceIdString, "fingerprint": Data(identityKeyDigest.prefix(4)).base64EncodedString()]
}
}
let request = OWSRequestFactory.batchIdentityCheckRequest(elements: elements)
return self.networkManager.makePromise(request: request)
}.done(on: schedulers.global()) { response in
guard response.responseStatusCode == 200 else {
throw OWSAssertionError("Unexpected response from batch identity request \(response.responseStatusCode)")
}
guard let json = response.responseBodyJson, let responseDictionary = json as? [String: AnyObject] else {
throw OWSAssertionError("Missing or invalid JSON")
}
guard let responseElements = responseDictionary["elements"] as? [[String: String]], !responseElements.isEmpty else {
return // No safety number changes
}
Logger.info("Detected \(responseElements.count) identity key changes via batch request")
self.db.write { tx in
for element in responseElements {
guard
let serviceIdString = element["uuid"],
let serviceId = try? ServiceId.parseFrom(serviceIdString: serviceIdString)
else {
owsFailDebug("Invalid uuid in batch identity response")
continue
}
guard
let encodedIdentityKey = element["identityKey"],
let externalIdentityKey = Data(base64Encoded: encodedIdentityKey),
let identityKey = try? IdentityKey(bytes: externalIdentityKey)
else {
owsFailDebug("Missing or invalid identity key in batch identity response")
continue
}
self.saveIdentityKey(identityKey, for: serviceId, tx: tx)
}
}
}.then { () -> Promise<Void> in
return self.batchUpdateIdentityKeys(for: remainingServiceIds)
}.catch { error in
owsFailDebug("Batch identity key update failed with error \(error)")
}
}
}
// MARK: - ObjC Bridge
class OWSIdentityManagerObjCBridge: NSObject {
@objc
static let identityKeyLength = UInt(OWSIdentityManagerImpl.Constants.identityKeyLength)
@objc
static let identityStateDidChangeNotification = NSNotification.Name.identityStateDidChange
@objc
static func identityKeyPair(forIdentity identity: OWSIdentity) -> ECKeyPair? {
return SSKEnvironment.shared.databaseStorageRef.read { tx in
let identityManager = DependenciesBridge.shared.identityManager
return identityManager.identityKeyPair(for: identity, tx: tx.asV2Read)
}
}
@objc
static func identityKey(forAddress address: SignalServiceAddress) -> Data? {
return SSKEnvironment.shared.databaseStorageRef.read { tx in
let identityManager = DependenciesBridge.shared.identityManager
return identityManager.identityKey(for: address, tx: tx.asV2Read)
}
}
@objc
static func saveIdentityKey(_ identityKey: Data, forServiceId serviceId: ServiceIdObjC, transaction tx: SDSAnyWriteTransaction) {
DependenciesBridge.shared.identityManager.saveIdentityKey(identityKey, for: serviceId.wrappedValue, tx: tx.asV2Write)
}
}
// MARK: - Unit Tests
#if TESTABLE_BUILD
extension OWSIdentityManager {
@discardableResult
func generateAndPersistNewIdentityKey(for identity: OWSIdentity) -> ECKeyPair {
let result = generateNewIdentityKeyPair()
DependenciesBridge.shared.db.write { tx in
setIdentityKeyPair(result, for: identity, tx: tx)
}
return result
}
}
#endif