session-ios/SignalUtilitiesKit/OWSUDManager.swift

505 lines
18 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
public enum OWSUDError: Error {
case assertionError(description: String)
case invalidData(description: String)
}
@objc
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
}
@objc
public enum UnidentifiedAccessMode: Int {
case unknown
case enabled
case disabled
case unrestricted
}
private func string(forUnidentifiedAccessMode mode: UnidentifiedAccessMode) -> String {
switch mode {
case .unknown:
return "unknown"
case .enabled:
return "enabled"
case .disabled:
return "disabled"
case .unrestricted:
return "unrestricted"
}
}
@objc
public class OWSUDAccess: NSObject {
@objc
public let udAccessKey: SMKUDAccessKey
@objc
public let udAccessMode: UnidentifiedAccessMode
@objc
public let isRandomKey: Bool
@objc
public required init(udAccessKey: SMKUDAccessKey,
udAccessMode: UnidentifiedAccessMode,
isRandomKey: Bool) {
self.udAccessKey = udAccessKey
self.udAccessMode = udAccessMode
self.isRandomKey = isRandomKey
}
}
@objc public protocol OWSUDManager: class {
@objc func setup()
@objc func trustRoot() -> ECPublicKey
@objc func isUDVerboseLoggingEnabled() -> Bool
// MARK: - Recipient State
@objc
func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, recipientId: String)
@objc
func unidentifiedAccessMode(forRecipientId recipientId: String) -> UnidentifiedAccessMode
@objc
func udAccessKey(forRecipientId recipientId: String) -> SMKUDAccessKey?
@objc
func udAccess(forRecipientId recipientId: String,
requireSyncAccess: Bool) -> OWSUDAccess?
// MARK: Sender Certificate
// We use completion handlers instead of a promise so that message sending
// logic can access the strongly typed certificate data.
@objc
func ensureSenderCertificate(success:@escaping (SMKSenderCertificate) -> Void,
failure:@escaping (Error) -> Void)
// MARK: Unrestricted Access
@objc
func shouldAllowUnrestrictedAccessLocal() -> Bool
@objc
func setShouldAllowUnrestrictedAccessLocal(_ value: Bool)
@objc
func getSenderCertificate() -> SMKSenderCertificate?
}
// MARK: -
@objc
public class OWSUDManagerImpl: NSObject, OWSUDManager {
private let dbConnection: YapDatabaseConnection
// MARK: Local Configuration State
private let kUDCollection = "kUDCollection"
private let kUDCurrentSenderCertificateKey_Production = "kUDCurrentSenderCertificateKey_Production"
private let kUDCurrentSenderCertificateKey_Staging = "kUDCurrentSenderCertificateKey_Staging"
private let kUDCurrentSenderCertificateDateKey_Production = "kUDCurrentSenderCertificateDateKey_Production"
private let kUDCurrentSenderCertificateDateKey_Staging = "kUDCurrentSenderCertificateDateKey_Staging"
private let kUDUnrestrictedAccessKey = "kUDUnrestrictedAccessKey"
// MARK: Recipient State
private let kUnidentifiedAccessCollection = "kUnidentifiedAccessCollection"
var certificateValidator: SMKCertificateValidator
@objc
public required init(primaryStorage: OWSPrimaryStorage) {
self.dbConnection = primaryStorage.newDatabaseConnection()
self.certificateValidator = SMKCertificateDefaultValidator(trustRoot: OWSUDManagerImpl.trustRoot())
super.init()
SwiftSingletons.register(self)
}
@objc public func setup() {
AppReadiness.runNowOrWhenAppDidBecomeReady {
guard self.tsAccountManager.isRegistered() else {
return
}
// Any error is silently ignored on startup.
self.ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete()
}
NotificationCenter.default.addObserver(self,
selector: #selector(registrationStateDidChange),
name: .RegistrationStateDidChange,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(didBecomeActive),
name: NSNotification.Name.OWSApplicationDidBecomeActive,
object: nil)
}
@objc
func registrationStateDidChange() {
AssertIsOnMainThread()
guard tsAccountManager.isRegisteredAndReady() else {
return
}
// Any error is silently ignored
ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete()
}
@objc func didBecomeActive() {
AssertIsOnMainThread()
AppReadiness.runNowOrWhenAppDidBecomeReady {
guard self.tsAccountManager.isRegistered() else {
return
}
// Any error is silently ignored on startup.
self.ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete()
}
}
// MARK: -
@objc
public func isUDVerboseLoggingEnabled() -> Bool {
return false
}
// MARK: - Dependencies
private var profileManager: ProfileManagerProtocol {
return SSKEnvironment.shared.profileManager
}
private var tsAccountManager: TSAccountManager {
return TSAccountManager.sharedInstance()
}
// MARK: - Recipient state
@objc
public func randomUDAccessKey() -> SMKUDAccessKey {
return SMKUDAccessKey(randomKeyData: ())
}
private func unidentifiedAccessMode(forRecipientId recipientId: String,
isLocalNumber: Bool,
transaction: YapDatabaseReadTransaction) -> UnidentifiedAccessMode {
let defaultValue: UnidentifiedAccessMode = isLocalNumber ? .enabled : .unknown
guard let existingRawValue = transaction.object(forKey: recipientId, inCollection: kUnidentifiedAccessCollection) as? Int else {
return defaultValue
}
guard let existingValue = UnidentifiedAccessMode(rawValue: existingRawValue) else {
owsFailDebug("Couldn't parse mode value.")
return defaultValue
}
return existingValue
}
@objc
public func unidentifiedAccessMode(forRecipientId recipientId: String) -> UnidentifiedAccessMode {
var isLocalNumber = false
if let localNumber = tsAccountManager.localNumber() {
isLocalNumber = recipientId == localNumber
}
var mode: UnidentifiedAccessMode = .unknown
dbConnection.read { (transaction) in
mode = self.unidentifiedAccessMode(forRecipientId: recipientId, isLocalNumber: isLocalNumber, transaction: transaction)
}
return mode
}
@objc
public func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, recipientId: String) {
var isLocalNumber = false
if let localNumber = tsAccountManager.localNumber() {
if recipientId == localNumber {
Logger.info("Setting local UD access mode: \(string(forUnidentifiedAccessMode: mode))")
isLocalNumber = true
}
}
Storage.writeSync { (transaction) in
let oldMode = self.unidentifiedAccessMode(forRecipientId: recipientId, isLocalNumber: isLocalNumber, transaction: transaction)
transaction.setObject(mode.rawValue as Int, forKey: recipientId, inCollection: self.kUnidentifiedAccessCollection)
if mode != oldMode {
Logger.info("Setting UD access mode for \(recipientId): \(string(forUnidentifiedAccessMode: oldMode)) -> \(string(forUnidentifiedAccessMode: mode))")
}
}
}
// Returns the UD access key for a given recipient
// if we have a valid profile key for them.
@objc
public func udAccessKey(forRecipientId recipientId: String) -> SMKUDAccessKey? {
guard let profileKey = profileManager.profileKeyData(forRecipientId: recipientId) else {
// Mark as "not a UD recipient".
return nil
}
do {
let udAccessKey = try SMKUDAccessKey(profileKey: profileKey)
return udAccessKey
} catch {
Logger.error("Could not determine udAccessKey: \(error)")
return nil
}
}
// Returns the UD access key for sending to a given recipient.
@objc
public func udAccess(forRecipientId recipientId: String,
requireSyncAccess: Bool) -> OWSUDAccess? {
if requireSyncAccess {
guard let localNumber = tsAccountManager.localNumber() else {
if isUDVerboseLoggingEnabled() {
Logger.info("UD disabled for \(recipientId), no local number.")
}
owsFailDebug("Missing local number.")
return nil
}
if localNumber != recipientId {
let selfAccessMode = unidentifiedAccessMode(forRecipientId: localNumber)
guard selfAccessMode != .disabled else {
if isUDVerboseLoggingEnabled() {
Logger.info("UD disabled for \(recipientId), UD disabled for sync messages.")
}
return nil
}
}
}
let accessMode = unidentifiedAccessMode(forRecipientId: recipientId)
switch accessMode {
case .unrestricted:
// Unrestricted users should use a random key.
if isUDVerboseLoggingEnabled() {
Logger.info("UD enabled for \(recipientId) with 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(forRecipientId: recipientId) {
if isUDVerboseLoggingEnabled() {
Logger.info("UD unknown for \(recipientId); trying derived key.")
}
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false)
} else {
if isUDVerboseLoggingEnabled() {
Logger.info("UD unknown for \(recipientId); trying random key.")
}
let udAccessKey = randomUDAccessKey()
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: true)
}
case .enabled:
guard let udAccessKey = udAccessKey(forRecipientId: recipientId) else {
if isUDVerboseLoggingEnabled() {
Logger.info("UD disabled for \(recipientId), no profile key for this recipient.")
}
if (!CurrentAppContext().isRunningTests) {
owsFailDebug("Couldn't find profile key for UD-enabled user.")
}
return nil
}
if isUDVerboseLoggingEnabled() {
Logger.info("UD enabled for \(recipientId).")
}
return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false)
case .disabled:
if isUDVerboseLoggingEnabled() {
Logger.info("UD disabled for \(recipientId), UD not enabled for this recipient.")
}
return nil
}
}
// MARK: - Sender Certificate
#if DEBUG
@objc
public func hasSenderCertificate() -> Bool {
return senderCertificate(certificateExpirationPolicy: .permissive) != nil
}
#endif
private func senderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> SMKSenderCertificate? {
if certificateExpirationPolicy == .strict {
guard let certificateDate = dbConnection.object(forKey: senderCertificateDateKey(), inCollection: kUDCollection) as? Date else {
return nil
}
guard certificateDate.timeIntervalSinceNow < kDayInterval else {
// Discard certificates that we obtained more than 24 hours ago.
return nil
}
}
guard let certificateData = dbConnection.object(forKey: senderCertificateKey(), inCollection: kUDCollection) as? Data else {
return nil
}
do {
let certificate = try SMKSenderCertificate.parse(data: certificateData)
guard isValidCertificate(certificate) else {
Logger.warn("Current sender certificate is not valid.")
return nil
}
return certificate
} catch {
owsFailDebug("Certificate could not be parsed: \(error)")
return nil
}
}
func setSenderCertificate(_ certificateData: Data) {
dbConnection.setObject(Date(), forKey: senderCertificateDateKey(), inCollection: kUDCollection)
dbConnection.setObject(certificateData, forKey: senderCertificateKey(), inCollection: kUDCollection)
}
private func senderCertificateKey() -> String {
return IsUsingProductionService() ? kUDCurrentSenderCertificateKey_Production : kUDCurrentSenderCertificateKey_Staging
}
private func senderCertificateDateKey() -> String {
return IsUsingProductionService() ? kUDCurrentSenderCertificateDateKey_Production : kUDCurrentSenderCertificateDateKey_Staging
}
@objc
public func ensureSenderCertificate(success:@escaping (SMKSenderCertificate) -> Void,
failure:@escaping (Error) -> Void) {
return ensureSenderCertificate(certificateExpirationPolicy: .permissive,
success: success,
failure: failure)
}
private func ensureSenderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy,
success:@escaping (SMKSenderCertificate) -> Void,
failure:@escaping (Error) -> Void) {
firstly {
ensureSenderCertificate(certificateExpirationPolicy: certificateExpirationPolicy)
}.map { certificate in
success(certificate)
}.catch { error in
failure(error)
}.retainUntilComplete()
}
public func ensureSenderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> Promise<SMKSenderCertificate> {
// Try to obtain a new sender certificate.
return firstly {
generateSenderCertificate()
}.map { (certificateData: Data, certificate: SMKSenderCertificate) in
// Cache the current sender certificate.
self.setSenderCertificate(certificateData)
return certificate
}
}
private func generateSenderCertificate() -> Promise<(certificateData: Data, certificate: SMKSenderCertificate)> {
return Promise<(certificateData: Data, certificate: SMKSenderCertificate)> { seal in
// Loki: Generate a sender certificate locally
let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: 1, senderRecipientId: sender)
let certificateAsData = try certificate.serialized()
guard isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate.")
}
seal.fulfill((certificateData: certificateAsData, certificate: certificate))
}
}
@objc
public func getSenderCertificate() -> SMKSenderCertificate? {
do {
let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: 1, senderRecipientId: sender)
guard self.isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server")
}
return certificate
} catch {
SNLog("Couldn't get UD sender certificate due to error: \(error).")
return nil
}
}
private func isValidCertificate(_ certificate: SMKSenderCertificate) -> Bool {
// 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
do {
try certificateValidator.throwswrapped_validate(senderCertificate: certificate, validationTime: anHourFromNowMs)
return true
} catch {
OWSLogger.error("Invalid certificate")
return false
}
}
@objc
public func trustRoot() -> ECPublicKey {
return OWSUDManagerImpl.trustRoot()
}
@objc
public class func trustRoot() -> ECPublicKey {
guard let trustRootData = NSData(fromBase64String: kUDTrustRoot) else {
// This exits.
owsFail("Invalid trust root data.")
}
do {
return try ECPublicKey(serializedKeyData: trustRootData as Data)
} catch {
// This exits.
owsFail("Invalid trust root.")
}
}
// MARK: - Unrestricted Access
@objc
public func shouldAllowUnrestrictedAccessLocal() -> Bool {
return dbConnection.bool(forKey: kUDUnrestrictedAccessKey, inCollection: kUDCollection, defaultValue: false)
}
@objc
public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) {
dbConnection.setBool(value, forKey: kUDUnrestrictedAccessKey, inCollection: kUDCollection)
// Try to update the account attributes to reflect this change.
tsAccountManager.updateAccountAttributes().retainUntilComplete()
}
}