515 lines
18 KiB
Swift
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) {
|
|
}
|
|
}
|