Signal-iOS/SignalServiceKit/Messages/UD/OWSUDManager.swift
2026-01-26 12:47:51 -06:00

515 lines
18 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
public enum OWSUDError: Error {
case assertionError(description: String)
case invalidData(description: String)
}
// 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: -
/// Represents an Unidentified Access Key that we believe may be valid.
///
/// "we believe may be valid": Our local state may be outdated, so the UAK
/// may be unauthorized when we use it. But we think it's worth trying.
///
/// If we're not sure (`.unknown`), or if we've previously confirmed it's
/// valid (`.enabled` & `.unrestricted`), we can create this type. If we've
/// previously confirmed it's not valid, we can't create this type.
public struct OWSUDAccess {
let key: SMKUDAccessKey
let mode: Mode
enum Mode {
case unknown
case enabled
case unrestricted
}
}
// MARK: -
public struct SenderCertificates {
let defaultCert: SenderCertificate
let uuidOnlyCert: SenderCertificate
}
// MARK: -
public protocol OWSUDManager {
var trustRoots: [PublicKey] { get }
// MARK: - Recipient State
func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, for aci: Aci, tx: DBWriteTransaction)
func udAccessKey(for aci: Aci, tx: DBReadTransaction) -> SMKUDAccessKey?
func udAccess(for aci: Aci, tx: DBReadTransaction) -> OWSUDAccess?
func fetchAllAciUakPairs(tx: DBReadTransaction) -> [Aci: SMKUDAccessKey]
// MARK: Sender Certificate
func fetchSenderCertificates() async throws -> SenderCertificates
func removeSenderCertificates(transaction: DBWriteTransaction)
func removeSenderCertificates(tx: DBWriteTransaction)
// MARK: Unrestricted Access
func shouldAllowUnrestrictedAccessLocal() -> Bool
func shouldAllowUnrestrictedAccessLocal(transaction: DBReadTransaction) -> Bool
func setShouldAllowUnrestrictedAccessLocal(_ value: Bool)
func setShouldAllowUnrestrictedAccessLocal(_ value: Bool, tx: DBWriteTransaction)
func phoneNumberSharingMode(tx: DBReadTransaction) -> PhoneNumberSharingMode?
func setPhoneNumberSharingMode(
_ mode: PhoneNumberSharingMode,
updateStorageServiceAndProfile: Bool,
tx: DBWriteTransaction,
)
}
// MARK: -
public class OWSUDManagerImpl: OWSUDManager {
private let keyValueStore = KeyValueStore(collection: "kUDCollection")
private let aciAccessStore = KeyValueStore(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 kUDUnrestrictedAccessKey = "kUDUnrestrictedAccessKey"
// MARK: Recipient State
private let db: any DB
private let tsAccountManager: any TSAccountManager
// Exposed for testing
public internal(set) var trustRoots: [PublicKey]
public init(
cron: Cron,
db: any DB,
tsAccountManager: any TSAccountManager,
) {
self.db = db
self.trustRoots = OWSUDManagerImpl.trustRoots()
self.tsAccountManager = tsAccountManager
SwiftSingletons.register(self)
// We can fill in any missing sender certificate async; message sending
// will fill in the sender certificate sooner if it needs it.
cron.schedulePeriodically(
uniqueKey: .fetchSenderCertificates,
approximateInterval: .day,
mustBeRegistered: true,
mustBeConnected: true,
operation: { _ = try await self.fetchSenderCertificates(forceRefresh: true) },
)
}
// MARK: - Recipient state
private func unidentifiedAccessMode(for aci: Aci, tx: DBReadTransaction) -> UnidentifiedAccessMode {
let existingValue: UnidentifiedAccessMode? = {
guard let rawValue = aciAccessStore.getInt(aci.serviceIdUppercaseString, transaction: tx) else {
return nil
}
return UnidentifiedAccessMode(rawValue: rawValue)
}()
return existingValue ?? .unknown
}
public func setUnidentifiedAccessMode(
_ mode: UnidentifiedAccessMode,
for aci: Aci,
tx: DBWriteTransaction,
) {
aciAccessStore.setInt(mode.rawValue, key: aci.serviceIdUppercaseString, transaction: tx)
}
public func fetchAllAciUakPairs(tx: DBReadTransaction) -> [Aci: SMKUDAccessKey] {
let acis: [Aci] = aciAccessStore.allKeys(transaction: tx).compactMap { aciString in
guard let aci = Aci.parseFrom(aciString: aciString) 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 aci: Aci, tx: DBReadTransaction) -> SMKUDAccessKey? {
let profileManager = SSKEnvironment.shared.profileManagerRef
guard let profileKey = profileManager.userProfile(for: SignalServiceAddress(aci), tx: tx)?.profileKey else {
return nil
}
return SMKUDAccessKey(profileKey: profileKey)
}
// Returns the UD access key for sending to a given recipient or fetching a profile
public func udAccess(for aci: Aci, tx: DBReadTransaction) -> OWSUDAccess? {
let accessKey: SMKUDAccessKey
let accessMode: OWSUDAccess.Mode
switch unidentifiedAccessMode(for: aci, tx: tx) {
case .unrestricted:
accessKey = .zeroedKey
accessMode = .unrestricted
case .unknown:
// If we're not sure, try our best to use the right key.
accessKey = udAccessKey(for: aci, tx: tx) ?? .zeroedKey
accessMode = .unknown
case .enabled:
guard let knownAccessKey = udAccessKey(for: aci, tx: tx) else {
// Shouldn't happen because we need a profile key to enable it.
Logger.warn("Missing profile key for UD-enabled user: \(aci)")
return nil
}
accessKey = knownAccessKey
accessMode = .enabled
case .disabled:
return nil
}
return OWSUDAccess(key: accessKey, mode: accessMode)
}
// MARK: - Sender Certificate
private func loadSenderCertificate(aciOnly: Bool) -> SenderCertificate? {
let dataValue = self.db.read { tx in
return self.keyValueStore.getData(self.senderCertificateKey(aciOnly: aciOnly), transaction: tx)
}
guard let dataValue else {
return nil
}
do {
let senderCertificate = try SenderCertificate(dataValue)
try validateCertificate(senderCertificate)
return senderCertificate
} catch {
Logger.warn("Ignoring invalid cached sender certificate: \(error)")
return nil
}
}
func setSenderCertificate(aciOnly: Bool, certificateData: Data) async {
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
self.keyValueStore.setData(certificateData, key: self.senderCertificateKey(aciOnly: aciOnly), transaction: tx)
}
}
public func removeSenderCertificates(transaction: DBWriteTransaction) {
keyValueStore.removeValue(forKey: senderCertificateKey(aciOnly: true), transaction: transaction)
keyValueStore.removeValue(forKey: senderCertificateKey(aciOnly: false), transaction: transaction)
}
public func removeSenderCertificates(tx: DBWriteTransaction) {
removeSenderCertificates(transaction: tx)
}
private func senderCertificateKey(aciOnly: Bool) -> String {
let baseKey = kUDCurrentSenderCertificateKey
if aciOnly {
return "\(baseKey)-withoutPhoneNumber"
} else {
return baseKey
}
}
private let fetchQueue = ConcurrentTaskQueue(concurrentLimit: 1)
public func fetchSenderCertificates() async throws -> SenderCertificates {
try await fetchSenderCertificates(forceRefresh: false)
}
private func fetchSenderCertificates(forceRefresh: Bool) async throws -> SenderCertificates {
return try await fetchQueue.run {
return try await _fetchSenderCertificates(forceRefresh: forceRefresh)
}
}
private func _fetchSenderCertificates(forceRefresh: Bool) async throws -> SenderCertificates {
_ = try self.tsAccountManager.registeredStateWithMaybeSneakyTransaction()
async let defaultCert = fetchSenderCertificate(aciOnly: false, forceRefresh: forceRefresh)
async let aciOnlyCert = fetchSenderCertificate(aciOnly: true, forceRefresh: forceRefresh)
return SenderCertificates(
defaultCert: try await defaultCert,
uuidOnlyCert: try await aciOnlyCert,
)
}
private func fetchSenderCertificate(aciOnly: Bool, forceRefresh: Bool) async throws -> SenderCertificate {
if !forceRefresh {
// If there is a valid cached sender certificate, use that.
if let certificate = loadSenderCertificate(aciOnly: aciOnly) {
return certificate
}
}
let senderCertificate: SenderCertificate
do {
senderCertificate = try await self.requestSenderCertificate(aciOnly: aciOnly)
} catch where error.isNetworkFailureOrTimeout || error.is5xxServiceResponse || error is CancellationError {
throw error
} catch {
Logger.warn("Couldn't fetch Sealed Sender certificate: \(error)")
SSKEnvironment.shared.notificationPresenterRef.notifyTestPopulation(ofErrorMessage: "Couldn't parse Sealed Sender certificate")
throw error
}
await self.setSenderCertificate(aciOnly: aciOnly, certificateData: senderCertificate.serialize())
return senderCertificate
}
private func requestSenderCertificate(aciOnly: Bool) async throws -> SenderCertificate {
let certificateRequest = OWSRequestFactory.udSenderCertificateRequest(uuidOnly: aciOnly)
let certificateResponse = try await SSKEnvironment.shared.networkManagerRef
.asyncRequest(certificateRequest)
let certificateData: Data = try {
guard let parser = certificateResponse.responseBodyParamParser else {
throw OWSUDError.invalidData(description: "Missing or invalid JSON")
}
return try parser.requiredBase64EncodedData(key: "certificate")
}()
let senderCertificate = try SenderCertificate(certificateData)
try validateCertificate(senderCertificate)
return senderCertificate
}
private func validateCertificate(_ certificate: SenderCertificate) throws {
guard
let deviceId = DeviceId(validating: certificate.deviceId),
self.tsAccountManager.storedDeviceIdWithMaybeTransaction.equals(deviceId)
else {
throw OWSUDError.invalidData(description: "Sender certificate has incorrect device ID")
}
let localIdentifiers = self.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction
let sender = certificate.sender
guard sender.e164 == nil || sender.e164 == localIdentifiers?.phoneNumber else {
throw OWSUDError.invalidData(description: "Sender certificate has incorrect phone number")
}
guard sender.senderAci == localIdentifiers!.aci else {
throw OWSUDError.invalidData(description: "Sender certificate has incorrect ACI")
}
// 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 + UInt64.hourInMs
guard certificate.validate(trustRoots: trustRoots, time: anHourFromNowMs) else {
throw OWSUDError.invalidData(description: "Sender certificate failed validation")
}
}
public class func trustRoots() -> [PublicKey] {
var trustRoots = [PublicKey]()
for trustRoot in TSConstants.kUDTrustRoots {
do {
guard let data = Data(base64Encoded: trustRoot) else {
// This exits.
owsFail("Invalid trust root data.")
}
trustRoots.append(try PublicKey(data))
} catch {
// This exits.
owsFail("Invalid trust root.")
}
}
return trustRoots
}
// MARK: - Unrestricted Access
public func shouldAllowUnrestrictedAccessLocal() -> Bool {
return self.db.read { transaction in
return self.shouldAllowUnrestrictedAccessLocal(transaction: transaction)
}
}
public func shouldAllowUnrestrictedAccessLocal(transaction: DBReadTransaction) -> Bool {
return self.keyValueStore.getBool(self.kUDUnrestrictedAccessKey, defaultValue: false, transaction: transaction)
}
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) {
self.db.write { transaction in
setShouldAllowUnrestrictedAccessLocal(value, tx: transaction)
}
}
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool, tx: DBWriteTransaction) {
self.keyValueStore.setBool(value, key: self.kUDUnrestrictedAccessKey, transaction: tx)
// Try to update the account attributes to reflect this change.
tx.addSyncCompletion {
Task {
do {
try await DependenciesBridge.shared.accountAttributesUpdater.updateAccountAttributes(authedAccount: .implicit())
} catch {
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: tx) else {
return nil
}
return PhoneNumberSharingMode(rawValue: rawMode)
}
public func setPhoneNumberSharingMode(
_ mode: PhoneNumberSharingMode,
updateStorageServiceAndProfile: Bool,
tx: DBWriteTransaction,
) {
keyValueStore.setInt(mode.rawValue, key: Self.phoneNumberSharingModeKey, transaction: tx)
if updateStorageServiceAndProfile {
tx.addSyncCompletion {
SSKEnvironment.shared.storageServiceManagerRef.recordPendingLocalAccountUpdates()
}
_ = SSKEnvironment.shared.profileManagerRef.reuploadLocalProfile(
unsavedRotatedProfileKey: nil,
mustReuploadAvatar: false,
authedAccount: .implicit(),
tx: tx,
)
}
}
}
// 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
}
}
public class OWSMockUDManager: OWSUDManager {
public var trustRoots: [LibSignalClient.PublicKey] = []
public func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, for aci: LibSignalClient.Aci, tx: DBWriteTransaction) {
}
public func udAccessKey(for aci: LibSignalClient.Aci, tx: DBReadTransaction) -> SMKUDAccessKey? {
return nil
}
public func udAccess(for aci: LibSignalClient.Aci, tx: DBReadTransaction) -> OWSUDAccess? {
return nil
}
public func fetchAllAciUakPairs(tx: DBReadTransaction) -> [LibSignalClient.Aci: SMKUDAccessKey] {
return [:]
}
public func fetchSenderCertificates() async throws -> SenderCertificates {
fatalError("not implemented")
}
public func removeSenderCertificates(transaction: DBWriteTransaction) {
}
public func removeSenderCertificates(tx: DBWriteTransaction) {
}
public func shouldAllowUnrestrictedAccessLocal() -> Bool {
return true
}
public func shouldAllowUnrestrictedAccessLocal(transaction: DBReadTransaction) -> Bool {
return true
}
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) {
}
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool, tx: DBWriteTransaction) {
}
public func phoneNumberSharingMode(tx: DBReadTransaction) -> PhoneNumberSharingMode? {
return nil
}
public func setPhoneNumberSharingMode(_ mode: PhoneNumberSharingMode, updateStorageServiceAndProfile: Bool, tx: DBWriteTransaction) {
}
}