Signal-iOS/SignalServiceKit/Messages/UD/OWSUDManager.swift
2024-05-07 17:04:54 -05:00

563 lines
20 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalCoreKit
import LibSignalClient
public enum OWSUDError: Error {
case assertionError(description: String)
case invalidData(description: String)
}
// MARK: -
extension OWSUDError: IsRetryableProvider {
public var isRetryableProvider: Bool {
switch self {
case .assertionError, .invalidData:
return false
}
}
}
// MARK: -
public enum OWSUDCertificateExpirationPolicy: Int {
// We want to try to rotate the sender certificate
// on a frequent basis, but we don't want to block
// sending on this.
case strict
case permissive
}
// MARK: -
public enum UnidentifiedAccessMode: Int {
case unknown
case enabled
case disabled
case unrestricted
}
// MARK: -
extension UnidentifiedAccessMode: CustomStringConvertible {
public var description: String {
switch self {
case .unknown:
return "unknown"
case .enabled:
return "enabled"
case .disabled:
return "disabled"
case .unrestricted:
return "unrestricted"
}
}
}
// MARK: -
public class OWSUDAccess: NSObject {
public let udAccessKey: SMKUDAccessKey
public var senderKeyUDAccessKey: SMKUDAccessKey {
// If unrestricted, we use a zeroed out key instead of a random key
// This ensures we don't scribble over the rest of our composite key when talking to the multi_recipient endpoint
udAccessMode == .unrestricted ? .zeroedKey : udAccessKey
}
public let udAccessMode: UnidentifiedAccessMode
public let isRandomKey: Bool
public init(udAccessKey: SMKUDAccessKey,
udAccessMode: UnidentifiedAccessMode,
isRandomKey: Bool) {
self.udAccessKey = udAccessKey
self.udAccessMode = udAccessMode
self.isRandomKey = isRandomKey
}
}
// MARK: -
public class SenderCertificates: NSObject {
let defaultCert: SenderCertificate
let uuidOnlyCert: SenderCertificate
init(defaultCert: SenderCertificate, uuidOnlyCert: SenderCertificate) {
self.defaultCert = defaultCert
self.uuidOnlyCert = uuidOnlyCert
}
}
// MARK: -
public class OWSUDSendingAccess: NSObject {
public let udAccess: OWSUDAccess
public let senderCertificate: SenderCertificate
init(udAccess: OWSUDAccess, senderCertificate: SenderCertificate) {
self.udAccess = udAccess
self.senderCertificate = senderCertificate
}
}
// MARK: -
public protocol OWSUDManager {
var trustRoot: PublicKey { get }
// MARK: - Recipient State
func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, for serviceId: ServiceId, tx: SDSAnyWriteTransaction)
func udAccessKey(for serviceId: ServiceId, tx: SDSAnyReadTransaction) -> SMKUDAccessKey?
func udAccess(for serviceId: ServiceId, tx: SDSAnyReadTransaction) -> OWSUDAccess?
func storyUdAccess() -> OWSUDAccess
func fetchAllAciUakPairs(tx: SDSAnyReadTransaction) -> [Aci: SMKUDAccessKey]
// MARK: Sender Certificate
func ensureSenderCertificates(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> Promise<SenderCertificates>
func removeSenderCertificates(transaction: SDSAnyWriteTransaction)
func removeSenderCertificates(tx: DBWriteTransaction)
// MARK: Unrestricted Access
func shouldAllowUnrestrictedAccessLocal() -> Bool
func shouldAllowUnrestrictedAccessLocal(transaction: SDSAnyReadTransaction) -> Bool
func setShouldAllowUnrestrictedAccessLocal(_ value: Bool)
func phoneNumberSharingMode(tx: DBReadTransaction) -> PhoneNumberSharingMode?
func setPhoneNumberSharingMode(
_ mode: PhoneNumberSharingMode,
updateStorageServiceAndProfile: Bool,
tx: SDSAnyWriteTransaction
)
}
// MARK: -
public class OWSUDManagerImpl: NSObject, OWSUDManager {
private let keyValueStore = SDSKeyValueStore(collection: "kUDCollection")
private let serviceIdAccessStore = SDSKeyValueStore(collection: "kUnidentifiedAccessUUIDCollection")
// MARK: Local Configuration State
// These keys contain the word "Production" for historical reasons, but
// they store sender certificates in both production & staging builds.
private let kUDCurrentSenderCertificateKey = "kUDCurrentSenderCertificateKey_Production-uuid"
private let kUDCurrentSenderCertificateDateKey = "kUDCurrentSenderCertificateDateKey_Production-uuid"
private let kUDUnrestrictedAccessKey = "kUDUnrestrictedAccessKey"
// MARK: Recipient State
// Exposed for testing
public internal(set) var trustRoot: PublicKey
public override init() {
self.trustRoot = OWSUDManagerImpl.trustRoot()
super.init()
SwiftSingletons.register(self)
AppReadiness.runNowOrWhenAppDidBecomeReadySync {
self.setup()
}
}
private func setup() {
owsAssertDebug(AppReadiness.isAppReady)
NotificationCenter.default.addObserver(self,
selector: #selector(registrationStateDidChange),
name: .registrationStateDidChange,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(didBecomeActive),
name: .OWSApplicationDidBecomeActive,
object: nil)
// We can fill in any missing sender certificate async;
// message sending will fill in the sender certificate sooner
// if it needs it.
DispatchQueue.global().async {
// Any error is silently ignored.
_ = self.ensureSenderCertificates(certificateExpirationPolicy: .strict)
}
}
@objc
private func registrationStateDidChange() {
AssertIsOnMainThread()
owsAssertDebug(AppReadiness.isAppReady)
// Any error is silently ignored.
_ = ensureSenderCertificates(certificateExpirationPolicy: .strict)
}
@objc
private func didBecomeActive() {
AssertIsOnMainThread()
owsAssertDebug(AppReadiness.isAppReady)
// Any error is silently ignored.
_ = ensureSenderCertificates(certificateExpirationPolicy: .strict)
}
// MARK: - Recipient state
private func randomUDAccessKey() -> SMKUDAccessKey {
return SMKUDAccessKey(randomKeyData: ())
}
private func unidentifiedAccessMode(for serviceId: ServiceId, tx: SDSAnyReadTransaction) -> UnidentifiedAccessMode {
let existingValue: UnidentifiedAccessMode? = {
guard let rawValue = serviceIdAccessStore.getInt(serviceId.serviceIdUppercaseString, transaction: tx) else {
return nil
}
return UnidentifiedAccessMode(rawValue: rawValue)
}()
return existingValue ?? .unknown
}
public func setUnidentifiedAccessMode(
_ mode: UnidentifiedAccessMode,
for serviceId: ServiceId,
tx: SDSAnyWriteTransaction
) {
serviceIdAccessStore.setInt(mode.rawValue, key: serviceId.serviceIdUppercaseString, transaction: tx)
}
public func fetchAllAciUakPairs(tx: SDSAnyReadTransaction) -> [Aci: SMKUDAccessKey] {
let acis: [Aci] = serviceIdAccessStore.allKeys(transaction: tx).compactMap { serviceIdString in
guard let aci = try? ServiceId.parseFrom(serviceIdString: serviceIdString) as? Aci else {
return nil
}
switch unidentifiedAccessMode(for: aci, tx: tx) {
case .enabled, .unrestricted, .unknown:
return aci
case .disabled:
return nil
}
}
var result = [Aci: SMKUDAccessKey]()
for aci in acis {
result[aci] = udAccessKey(for: aci, tx: tx)
}
return result
}
// Returns the UD access key for a given recipient
// if we have a valid profile key for them.
public func udAccessKey(for serviceId: ServiceId, tx: SDSAnyReadTransaction) -> SMKUDAccessKey? {
guard let profileKey = profileManager.profileKeyData(for: SignalServiceAddress(serviceId), transaction: tx) else {
return nil
}
do {
return try SMKUDAccessKey(profileKey: profileKey)
} catch {
Logger.error("Could not determine udAccessKey: \(error)")
return nil
}
}
// Returns the UD access key for sending to a given recipient or fetching a profile
public func udAccess(for serviceId: ServiceId, tx: SDSAnyReadTransaction) -> OWSUDAccess? {
let accessMode = unidentifiedAccessMode(for: serviceId, tx: tx)
switch accessMode {
case .unrestricted:
// Unrestricted users should use a random key.
let udAccessKey = randomUDAccessKey()
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: true)
case .unknown:
// Unknown users should use a derived key if possible,
// and otherwise use a random key.
if let udAccessKey = udAccessKey(for: serviceId, tx: tx) {
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false)
} else {
let udAccessKey = randomUDAccessKey()
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: true)
}
case .enabled:
guard let udAccessKey = udAccessKey(for: serviceId, tx: tx) else {
// Not an error.
// We can only use UD if the user has UD enabled _and_
// we know their profile key.
Logger.warn("Missing profile key for UD-enabled user: \(serviceId).")
return nil
}
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false)
case .disabled:
return nil
}
}
public func storyUdAccess() -> OWSUDAccess {
return OWSUDAccess(udAccessKey: randomUDAccessKey(), udAccessMode: .unrestricted, isRandomKey: true)
}
// MARK: - Sender Certificate
#if TESTABLE_BUILD
public func hasSenderCertificates() -> Bool {
return senderCertificate(uuidOnly: true, certificateExpirationPolicy: .permissive) != nil
&& senderCertificate(uuidOnly: false, certificateExpirationPolicy: .permissive) != nil
}
#endif
private func senderCertificate(uuidOnly: Bool, certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> SenderCertificate? {
var certificateDateValue: Date?
var certificateDataValue: Data?
databaseStorage.read { transaction in
certificateDateValue = self.keyValueStore.getDate(self.senderCertificateDateKey(uuidOnly: uuidOnly), transaction: transaction)
certificateDataValue = self.keyValueStore.getData(self.senderCertificateKey(uuidOnly: uuidOnly), transaction: transaction)
}
if certificateExpirationPolicy == .strict {
guard let certificateDate = certificateDateValue else {
return nil
}
guard -certificateDate.timeIntervalSinceNow < kDayInterval else {
// Discard certificates that we obtained more than 24 hours ago.
return nil
}
}
guard let certificateData = certificateDataValue else {
return nil
}
do {
let certificate = try SenderCertificate(certificateData)
guard isValidCertificate(certificate) else {
Logger.info("Existing sender certificate isn't valid. Ignoring it and fetching a new one...")
return nil
}
return certificate
} catch {
owsFailDebug("Certificate could not be parsed: \(error)")
return nil
}
}
func setSenderCertificate(uuidOnly: Bool, certificateData: Data) {
databaseStorage.write { transaction in
self.keyValueStore.setDate(Date(), key: self.senderCertificateDateKey(uuidOnly: uuidOnly), transaction: transaction)
self.keyValueStore.setData(certificateData, key: self.senderCertificateKey(uuidOnly: uuidOnly), transaction: transaction)
}
}
public func removeSenderCertificates(transaction: SDSAnyWriteTransaction) {
keyValueStore.removeValue(forKey: senderCertificateDateKey(uuidOnly: true), transaction: transaction)
keyValueStore.removeValue(forKey: senderCertificateKey(uuidOnly: true), transaction: transaction)
keyValueStore.removeValue(forKey: senderCertificateDateKey(uuidOnly: false), transaction: transaction)
keyValueStore.removeValue(forKey: senderCertificateKey(uuidOnly: false), transaction: transaction)
}
public func removeSenderCertificates(tx: DBWriteTransaction) {
removeSenderCertificates(transaction: SDSDB.shimOnlyBridge(tx))
}
private func senderCertificateKey(uuidOnly: Bool) -> String {
let baseKey = kUDCurrentSenderCertificateKey
if uuidOnly {
return "\(baseKey)-withoutPhoneNumber"
} else {
return baseKey
}
}
private func senderCertificateDateKey(uuidOnly: Bool) -> String {
let baseKey = kUDCurrentSenderCertificateDateKey
if uuidOnly {
return "\(baseKey)-withoutPhoneNumber"
} else {
return baseKey
}
}
public func ensureSenderCertificates(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> Promise<SenderCertificates> {
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
// We don't want to assert but we should log and fail.
return Promise(error: OWSGenericError("Not registered and ready."))
}
let defaultPromise = ensureSenderCertificate(uuidOnly: false, certificateExpirationPolicy: certificateExpirationPolicy)
let uuidOnlyPromise = ensureSenderCertificate(uuidOnly: true, certificateExpirationPolicy: certificateExpirationPolicy)
return firstly(on: DispatchQueue.global()) {
Promise.when(fulfilled: defaultPromise, uuidOnlyPromise)
}.map(on: DispatchQueue.global()) { defaultCert, uuidOnlyCert in
return SenderCertificates(defaultCert: defaultCert, uuidOnlyCert: uuidOnlyCert)
}
}
public func ensureSenderCertificate(uuidOnly: Bool, certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> Promise<SenderCertificate> {
// If there is a valid cached sender certificate, use that.
if let certificate = senderCertificate(uuidOnly: uuidOnly, certificateExpirationPolicy: certificateExpirationPolicy) {
return Promise.value(certificate)
}
return firstly(on: DispatchQueue.global()) {
self.requestSenderCertificate(uuidOnly: uuidOnly)
}.map(on: DispatchQueue.global()) { (certificate: SenderCertificate) in
self.setSenderCertificate(uuidOnly: uuidOnly, certificateData: Data(certificate.serialize()))
return certificate
}
}
private func requestSenderCertificate(uuidOnly: Bool) -> Promise<SenderCertificate> {
return firstly(on: DispatchQueue.global()) {
SignalServiceRestClient().requestUDSenderCertificate(uuidOnly: uuidOnly)
}.map(on: DispatchQueue.global()) { (certificateData: Data) -> SenderCertificate in
let certificate = try SenderCertificate(certificateData)
guard self.isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server")
}
return certificate
}.recover(on: DispatchQueue.global()) { error -> Promise<SenderCertificate> in
throw error
}
}
private func isValidCertificate(_ certificate: SenderCertificate) -> Bool {
let sender = certificate.sender
guard sender.deviceId == DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction else {
Logger.warn("Sender certificate has incorrect device ID")
return false
}
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction
guard sender.e164 == nil || sender.e164 == localIdentifiers?.phoneNumber else {
Logger.warn("Sender certificate has incorrect phone number")
return false
}
guard sender.senderAci == localIdentifiers!.aci else {
Logger.error("Sender certificate has incorrect ACI")
return false
}
// Ensure that the certificate will not expire in the next hour.
// We want a threshold long enough to ensure that any outgoing message
// sends will complete before the expiration.
let nowMs = NSDate.ows_millisecondTimeStamp()
let anHourFromNowMs = nowMs + kHourInMs
guard case .some(true) = try? certificate.validate(trustRoot: trustRoot, time: anHourFromNowMs) else {
return false
}
return true
}
public class func trustRoot() -> PublicKey {
guard let trustRootData = Data(base64Encoded: TSConstants.kUDTrustRoot) else {
// This exits.
owsFail("Invalid trust root data.")
}
do {
return try PublicKey(trustRootData as Data)
} catch {
// This exits.
owsFail("Invalid trust root.")
}
}
// MARK: - Unrestricted Access
public func shouldAllowUnrestrictedAccessLocal() -> Bool {
return databaseStorage.read { transaction in
return self.shouldAllowUnrestrictedAccessLocal(transaction: transaction)
}
}
public func shouldAllowUnrestrictedAccessLocal(transaction: SDSAnyReadTransaction) -> Bool {
return self.keyValueStore.getBool(self.kUDUnrestrictedAccessKey, defaultValue: false, transaction: transaction)
}
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) {
databaseStorage.write { transaction in
self.keyValueStore.setBool(value, key: self.kUDUnrestrictedAccessKey, transaction: transaction)
}
// Try to update the account attributes to reflect this change.
firstly(on: DispatchQueue.global()) {
return Promise.wrapAsync {
try await DependenciesBridge.shared.accountAttributesUpdater.updateAccountAttributes(authedAccount: .implicit())
}
}.catch(on: DispatchQueue.global()) { error in
Logger.warn("Error: \(error)")
}
}
// MARK: - Phone Number Sharing
private static var phoneNumberSharingModeKey: String { "phoneNumberSharingMode" }
public func phoneNumberSharingMode(tx: DBReadTransaction) -> PhoneNumberSharingMode? {
guard let rawMode = keyValueStore.getInt(Self.phoneNumberSharingModeKey, transaction: SDSDB.shimOnlyBridge(tx)) else {
return nil
}
return PhoneNumberSharingMode(rawValue: rawMode)
}
public func setPhoneNumberSharingMode(
_ mode: PhoneNumberSharingMode,
updateStorageServiceAndProfile: Bool,
tx: SDSAnyWriteTransaction
) {
keyValueStore.setInt(mode.rawValue, key: Self.phoneNumberSharingModeKey, transaction: tx)
if updateStorageServiceAndProfile {
tx.addSyncCompletion {
Self.storageServiceManager.recordPendingLocalAccountUpdates()
}
_ = profileManager.reuploadLocalProfile(
unsavedRotatedProfileKey: nil,
mustReuploadAvatar: false,
authedAccount: .implicit(),
tx: tx.asV2Write
)
}
}
}
// MARK: -
/// These are persisted to disk, so they must remain stable.
public enum PhoneNumberSharingMode: Int {
case everybody = 0
case nobody = 2
public static let defaultValue: PhoneNumberSharingMode = .nobody
}
extension Optional where Wrapped == PhoneNumberSharingMode {
public var orDefault: PhoneNumberSharingMode {
return self ?? .defaultValue
}
}