import Foundation #if os(macOS) import PostboxMac import SwiftSignalKitMac import MtProtoKitMac #else import Postbox import SwiftSignalKit import MtProtoKitDynamic #endif import TelegramCorePrivateModule public protocol AccountState: PostboxCoding { func equalsTo(_ other: AccountState) -> Bool } public func ==(lhs: AccountState, rhs: AccountState) -> Bool { return lhs.equalsTo(rhs) } public class AuthorizedAccountState: AccountState { public final class State: PostboxCoding, Equatable, CustomStringConvertible { let pts: Int32 let qts: Int32 let date: Int32 let seq: Int32 init(pts: Int32, qts: Int32, date: Int32, seq: Int32) { self.pts = pts self.qts = qts self.date = date self.seq = seq } public init(decoder: PostboxDecoder) { self.pts = decoder.decodeInt32ForKey("pts", orElse: 0) self.qts = decoder.decodeInt32ForKey("qts", orElse: 0) self.date = decoder.decodeInt32ForKey("date", orElse: 0) self.seq = decoder.decodeInt32ForKey("seq", orElse: 0) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.pts, forKey: "pts") encoder.encodeInt32(self.qts, forKey: "qts") encoder.encodeInt32(self.date, forKey: "date") encoder.encodeInt32(self.seq, forKey: "seq") } public var description: String { return "(pts: \(pts), qts: \(qts), seq: \(seq), date: \(date))" } } let isTestingEnvironment: Bool let masterDatacenterId: Int32 let peerId: PeerId let state: State? public required init(decoder: PostboxDecoder) { self.isTestingEnvironment = decoder.decodeInt32ForKey("isTestingEnvironment", orElse: 0) != 0 self.masterDatacenterId = decoder.decodeInt32ForKey("masterDatacenterId", orElse: 0) self.peerId = PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0)) self.state = decoder.decodeObjectForKey("state", decoder: { return State(decoder: $0) }) as? State } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.isTestingEnvironment ? 1 : 0, forKey: "isTestingEnvironment") encoder.encodeInt32(self.masterDatacenterId, forKey: "masterDatacenterId") encoder.encodeInt64(self.peerId.toInt64(), forKey: "peerId") if let state = self.state { encoder.encodeObject(state, forKey: "state") } } public init(isTestingEnvironment: Bool, masterDatacenterId: Int32, peerId: PeerId, state: State?) { self.isTestingEnvironment = isTestingEnvironment self.masterDatacenterId = masterDatacenterId self.peerId = peerId self.state = state } func changedState(_ state: State) -> AuthorizedAccountState { return AuthorizedAccountState(isTestingEnvironment: self.isTestingEnvironment, masterDatacenterId: self.masterDatacenterId, peerId: self.peerId, state: state) } public func equalsTo(_ other: AccountState) -> Bool { if let other = other as? AuthorizedAccountState { return self.isTestingEnvironment == other.isTestingEnvironment && self.masterDatacenterId == other.masterDatacenterId && self.peerId == other.peerId && self.state == other.state } else { return false } } } public func ==(lhs: AuthorizedAccountState.State, rhs: AuthorizedAccountState.State) -> Bool { return lhs.pts == rhs.pts && lhs.qts == rhs.qts && lhs.date == rhs.date && lhs.seq == rhs.seq } public class UnauthorizedAccount { public let networkArguments: NetworkInitializationArguments public let id: AccountRecordId public let rootPath: String public let basePath: String public let testingEnvironment: Bool public let postbox: Postbox public let network: Network public var masterDatacenterId: Int32 { return Int32(self.network.mtProto.datacenterId) } public let shouldBeServiceTaskMaster = Promise() init(networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) { self.networkArguments = networkArguments self.id = id self.rootPath = rootPath self.basePath = basePath self.testingEnvironment = testingEnvironment self.postbox = postbox self.network = network network.shouldKeepConnection.set(self.shouldBeServiceTaskMaster.get() |> map { mode -> Bool in switch mode { case .now, .always: return true case .never: return false } }) } public func changedMasterDatacenterId(_ masterDatacenterId: Int32) -> Signal { if masterDatacenterId == Int32(self.network.mtProto.datacenterId) { return .single(self) } else { let postbox = self.postbox let keychain = Keychain(get: { key in return postbox.keychainEntryForKey(key) }, set: { (key, data) in postbox.setKeychainEntryForKey(key, value: data) }, remove: { key in postbox.removeKeychainEntryForKey(key) }) return self.postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?) in return (transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings, transaction.getPreferencesEntry(key: PreferencesKeys.proxySettings) as? ProxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings) as? NetworkSettings) } |> mapToSignal { (localizationSettings, proxySettings, networkSettings) -> Signal in return initializedNetwork(arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil) |> map { network in let updated = UnauthorizedAccount(networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network) updated.shouldBeServiceTaskMaster.set(self.shouldBeServiceTaskMaster.get()) return updated } } } } } func accountNetworkUsageInfoPath(basePath: String) -> String { return basePath + "/network-usage" } public func accountRecordIdPathName(_ id: AccountRecordId) -> String { return "account-\(UInt64(bitPattern: id.int64))" } public enum AccountResult { case upgrading case unauthorized(UnauthorizedAccount) case authorized(Account) } let telegramPostboxSeedConfiguration: SeedConfiguration = { var initializeMessageNamespacesWithHoles: [(PeerId.Namespace, MessageId.Namespace)] = [] for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles { initializeMessageNamespacesWithHoles.append((peerNamespace, Namespaces.Message.Cloud)) } return SeedConfiguration(initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: 1))), initializeMessageNamespacesWithHoles: initializeMessageNamespacesWithHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer in if let peer = peer as? TelegramChannel { switch peer.info { case .group: if let addressName = peer.addressName, !addressName.isEmpty { return [.publicGroups] } else { return [.regularChatsAndPrivateGroups] } case .broadcast: return [.channels] } } else { return [.regularChatsAndPrivateGroups] } }) }() public func accountWithId(networkArguments: NetworkInitializationArguments, id: AccountRecordId, supplementary: Bool, rootPath: String, beginWithTestingEnvironment: Bool, auxiliaryMethods: AccountAuxiliaryMethods, shouldKeepAutoConnection: Bool = true) -> Signal { let path = "\(rootPath)/\(accountRecordIdPathName(id))" let postbox = openPostbox(basePath: path + "/postbox", globalMessageIdsNamespace: Namespaces.Message.Cloud, seedConfiguration: telegramPostboxSeedConfiguration) return postbox |> mapToSignal { result -> Signal in switch result { case .upgrading: return .single(.upgrading) case let .postbox(postbox): return postbox.stateView() |> take(1) |> mapToSignal { view -> Signal in return postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?) in return (transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings, transaction.getPreferencesEntry(key: PreferencesKeys.proxySettings) as? ProxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings) as? NetworkSettings) } |> mapToSignal { (localizationSettings, proxySettings, networkSettings) -> Signal in let accountState = view.state let keychain = Keychain(get: { key in return postbox.keychainEntryForKey(key) }, set: { (key, data) in postbox.setKeychainEntryForKey(key, value: data) }, remove: { key in postbox.removeKeychainEntryForKey(key) }) if let accountState = accountState { switch accountState { case let unauthorizedState as UnauthorizedAccountState: return initializedNetwork(arguments: networkArguments, supplementary: supplementary, datacenterId: Int(unauthorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil) |> map { network -> AccountResult in return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } case let authorizedState as AuthorizedAccountState: return postbox.transaction { transaction -> String? in return (transaction.getPeer(authorizedState.peerId) as? TelegramUser)?.phone } |> mapToSignal { phoneNumber in return initializedNetwork(arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber) |> map { network -> AccountResult in return .authorized(Account(id: id, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, postbox: postbox, network: network, networkArguments: networkArguments, peerId: authorizedState.peerId, auxiliaryMethods: auxiliaryMethods)) } } case _: assertionFailure("Unexpected accountState \(accountState)") } } return initializedNetwork(arguments: networkArguments, supplementary: supplementary, datacenterId: 2, keychain: keychain, basePath: path, testingEnvironment: beginWithTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil) |> map { network -> AccountResult in return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } } } } } } public enum TwoStepPasswordDerivation { case unknown case sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1: Data, salt2: Data, iterations: Int32, g: Int32, p: Data) fileprivate init(_ apiAlgo: Api.PasswordKdfAlgo) { switch apiAlgo { case .passwordKdfAlgoUnknown: self = .unknown case let .passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow(salt1, salt2, g, p): self = .sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1: salt1.makeData(), salt2: salt2.makeData(), iterations: 100000, g: g, p: p.makeData()) } } var apiAlgo: Api.PasswordKdfAlgo { switch self { case .unknown: return .passwordKdfAlgoUnknown case let .sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1, salt2, iterations, g, p): precondition(iterations == 100000) return .passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow(salt1: Buffer(data: salt1), salt2: Buffer(data: salt2), g: g, p: Buffer(data: p)) } } } public enum TwoStepSecurePasswordDerivation { case unknown case sha512(salt: Data) case PBKDF2_HMAC_sha512(salt: Data, iterations: Int32) init(_ apiAlgo: Api.SecurePasswordKdfAlgo) { switch apiAlgo { case .securePasswordKdfAlgoUnknown: self = .unknown case let .securePasswordKdfAlgoPBKDF2HMACSHA512iter100000(salt): self = .PBKDF2_HMAC_sha512(salt: salt.makeData(), iterations: 100000) case let .securePasswordKdfAlgoSHA512(salt): self = .sha512(salt: salt.makeData()) } } var apiAlgo: Api.SecurePasswordKdfAlgo { switch self { case .unknown: return .securePasswordKdfAlgoUnknown case let .PBKDF2_HMAC_sha512(salt, iterations): precondition(iterations == 100000) return .securePasswordKdfAlgoPBKDF2HMACSHA512iter100000(salt: Buffer(data: salt)) case let .sha512(salt): return .securePasswordKdfAlgoSHA512(salt: Buffer(data: salt)) } } } public struct TwoStepSRPSessionData { public let id: Int64 public let B: Data } public struct TwoStepAuthData { public let nextPasswordDerivation: TwoStepPasswordDerivation public let currentPasswordDerivation: TwoStepPasswordDerivation? public let srpSessionData: TwoStepSRPSessionData? public let hasRecovery: Bool public let hasSecretValues: Bool public let currentHint: String? public let unconfirmedEmailPattern: String? public let secretRandom: Data public let nextSecurePasswordDerivation: TwoStepSecurePasswordDerivation } public func twoStepAuthData(_ network: Network) -> Signal { return network.request(Api.functions.account.getPassword()) |> map { config -> TwoStepAuthData in switch config { case let .password(flags, currentAlgo, srpB, srpId, hint, emailUnconfirmedPattern, newAlgo, newSecureAlgo, secureRandom): let hasRecovery = (flags & (1 << 0)) != 0 let hasSecureValues = (flags & (1 << 1)) != 0 let currentDerivation = currentAlgo.flatMap(TwoStepPasswordDerivation.init) let nextDerivation = TwoStepPasswordDerivation(newAlgo) let nextSecureDerivation = TwoStepSecurePasswordDerivation(newSecureAlgo) switch nextSecureDerivation { case .unknown: break case .PBKDF2_HMAC_sha512: break case .sha512: preconditionFailure() } var srpSessionData: TwoStepSRPSessionData? if let srpB = srpB, let srpId = srpId { srpSessionData = TwoStepSRPSessionData(id: srpId, B: srpB.makeData()) } return TwoStepAuthData(nextPasswordDerivation: nextDerivation, currentPasswordDerivation: currentDerivation, srpSessionData: srpSessionData, hasRecovery: hasRecovery, hasSecretValues: hasSecureValues, currentHint: hint, unconfirmedEmailPattern: emailUnconfirmedPattern, secretRandom: secureRandom.makeData(), nextSecurePasswordDerivation: nextSecureDerivation) } } } public func hexString(_ data: Data) -> String { let hexString = NSMutableString() data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in for i in 0 ..< data.count { hexString.appendFormat("%02x", UInt(bytes.advanced(by: i).pointee)) } } return hexString as String } public func dataWithHexString(_ string: String) -> Data { var hex = string if hex.count % 2 != 0 { return Data() } var data = Data() while hex.count > 0 { let subIndex = hex.index(hex.startIndex, offsetBy: 2) let c = String(hex[.. Data { return data.withUnsafeBytes { bytes -> Data in return CryptoSHA1(bytes, Int32(data.count)) } } func sha256Digest(_ data : Data) -> Data { return data.withUnsafeBytes { bytes -> Data in return CryptoSHA256(bytes, Int32(data.count)) } } func sha512Digest(_ data : Data) -> Data { return data.withUnsafeBytes { bytes -> Data in return CryptoSHA512(bytes, Int32(data.count)) } } func passwordUpdateKDF(password: String, derivation: TwoStepPasswordDerivation) -> (Data, TwoStepPasswordDerivation)? { guard let passwordData = password.data(using: .utf8, allowLossyConversion: true) else { return nil } switch derivation { case .unknown: return nil case let .sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1, salt2, iterations, gValue, p): var nextSalt1 = salt1 var randomSalt1 = Data() randomSalt1.count = 32 randomSalt1.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in arc4random_buf(bytes, 32) } nextSalt1.append(randomSalt1) let nextSalt2 = salt2 var g = Data(count: 4) g.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in var gValue = gValue withUnsafeBytes(of: &gValue, { (sourceBuffer: UnsafeRawBufferPointer) -> Void in let sourceBytes = sourceBuffer.bindMemory(to: Int8.self).baseAddress! for i in 0 ..< 4 { bytes.advanced(by: i).pointee = sourceBytes.advanced(by: 4 - i - 1).pointee } }) } let pbkdfInnerData = sha256Digest(nextSalt2 + sha256Digest(nextSalt1 + passwordData + nextSalt1) + nextSalt2) guard let pbkdfResult = MTPBKDF2(pbkdfInnerData, nextSalt1, iterations) else { return nil } let x = sha256Digest(nextSalt2 + pbkdfResult + nextSalt2) let gx = MTExp(g, x, p)! return (gx, .sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1: nextSalt1, salt2: nextSalt2, iterations: iterations, g: gValue, p: p)) } } struct PasswordKDFResult { let id: Int64 let A: Data let M1: Data } private func paddedToLength(what: Data, to: Data) -> Data { if what.count < to.count { var what = what for _ in 0 ..< to.count - what.count { what.insert(0, at: 0) } return what } else { return what } } private func paddedXor(_ a: Data, _ b: Data) -> Data { let count = max(a.count, b.count) var a = a var b = b while a.count < count { a.insert(0, at: 0) } while b.count < count { b.insert(0, at: 0) } a.withUnsafeMutableBytes { (aBytes: UnsafeMutablePointer) -> Void in b.withUnsafeBytes { (bBytes: UnsafePointer) -> Void in for i in 0 ..< count { aBytes.advanced(by: i).pointee = aBytes.advanced(by: i).pointee ^ bBytes.advanced(by: i).pointee } } } return a } func passwordKDF(password: String, derivation: TwoStepPasswordDerivation, srpSessionData: TwoStepSRPSessionData) -> PasswordKDFResult? { guard let passwordData = password.data(using: .utf8, allowLossyConversion: true) else { return nil } switch derivation { case .unknown: return nil case let .sha256_sha256_PBKDF2_HMAC_sha512_sha256_srp(salt1, salt2, iterations, gValue, p): var a = Data(count: p.count) let aLength = a.count a.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in let _ = SecRandomCopyBytes(nil, aLength, bytes) } var g = Data(count: 4) g.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in var gValue = gValue withUnsafeBytes(of: &gValue, { (sourceBuffer: UnsafeRawBufferPointer) -> Void in let sourceBytes = sourceBuffer.bindMemory(to: Int8.self).baseAddress! for i in 0 ..< 4 { bytes.advanced(by: i).pointee = sourceBytes.advanced(by: 4 - i - 1).pointee } }) } if !MTCheckIsSafeB(srpSessionData.B, p) { return nil } let B = paddedToLength(what: srpSessionData.B, to: p) let A = paddedToLength(what: MTExp(g, a, p)!, to: p) let u = sha256Digest(A + B) if MTIsZero(u) { return nil } let pbkdfInnerData = sha256Digest(salt2 + sha256Digest(salt1 + passwordData + salt1) + salt2) guard let pbkdfResult = MTPBKDF2(pbkdfInnerData, salt1, iterations) else { return nil } let x = sha256Digest(salt2 + pbkdfResult + salt2) let gx = MTExp(g, x, p)! let k = sha256Digest(p + paddedToLength(what: g, to: p)) let s1 = MTModSub(B, MTModMul(k, gx, p)!, p)! if !MTCheckIsSafeGAOrB(s1, p) { return nil } let s2 = MTAdd(a, MTMul(u, x)!)! let S = MTExp(s1, s2, p)! let K = sha256Digest(paddedToLength(what: S, to: p)) let m1 = paddedXor(sha256Digest(p), sha256Digest(paddedToLength(what: g, to: p))) let m2 = sha256Digest(salt1) let m3 = sha256Digest(salt2) let M = sha256Digest(m1 + m2 + m3 + A + B + K) return PasswordKDFResult(id: srpSessionData.id, A: A, M1: M) } } func securePasswordUpdateKDF(password: String, derivation: TwoStepSecurePasswordDerivation) -> (Data, TwoStepSecurePasswordDerivation)? { guard let passwordData = password.data(using: .utf8, allowLossyConversion: true) else { return nil } switch derivation { case .unknown: return nil case let .sha512(salt): var nextSalt = salt var randomSalt = Data() randomSalt.count = 32 randomSalt.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in arc4random_buf(bytes, 32) } nextSalt.append(randomSalt) var data = Data() data.append(nextSalt) data.append(passwordData) data.append(nextSalt) return (sha512Digest(data), .sha512(salt: nextSalt)) case let .PBKDF2_HMAC_sha512(salt, iterations): var nextSalt = salt var randomSalt = Data() randomSalt.count = 32 randomSalt.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in arc4random_buf(bytes, 32) } nextSalt.append(randomSalt) guard let passwordHash = MTPBKDF2(passwordData, nextSalt, iterations) else { return nil } return (passwordHash, .PBKDF2_HMAC_sha512(salt: nextSalt, iterations: iterations)) } } func securePasswordKDF(password: String, derivation: TwoStepSecurePasswordDerivation) -> Data? { guard let passwordData = password.data(using: .utf8, allowLossyConversion: true) else { return nil } switch derivation { case .unknown: return nil case let .sha512(salt): var data = Data() data.append(salt) data.append(passwordData) data.append(salt) return sha512Digest(data) case let .PBKDF2_HMAC_sha512(salt, iterations): guard let passwordHash = MTPBKDF2(passwordData, salt, iterations) else { return nil } return passwordHash } } func verifyPassword(_ account: UnauthorizedAccount, password: String) -> Signal { return twoStepAuthData(account.network) |> mapToSignal { authData -> Signal in guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else { return .fail(MTRpcError(errorCode: 400, errorDescription: "INTERNAL_NO_PASSWORD")) } let kdfResult = passwordKDF(password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) if let kdfResult = kdfResult { return account.network.request(Api.functions.auth.checkPassword(password: .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))), automaticFloodWait: false) } else { return .fail(MTRpcError(errorCode: 400, errorDescription: "KDF_ERROR")) } } } public enum AccountServiceTaskMasterMode { case now case always case never } public struct AccountNetworkProxyState: Equatable { public let address: String public let hasConnectionIssues: Bool } public enum AccountNetworkState: Equatable { case waitingForNetwork case connecting(proxy: AccountNetworkProxyState?) case updating(proxy: AccountNetworkProxyState?) case online(proxy: AccountNetworkProxyState?) } public final class AccountAuxiliaryMethods { public let updatePeerChatInputState: (PeerChatInterfaceState?, SynchronizeableChatInputState?) -> PeerChatInterfaceState? public let fetchResource: (Account, MediaResource, Signal<[(Range, MediaBoxFetchPriority)], NoError>, MediaResourceFetchParameters?) -> Signal? public let fetchResourceMediaReferenceHash: (MediaResource) -> Signal public init(updatePeerChatInputState: @escaping (PeerChatInterfaceState?, SynchronizeableChatInputState?) -> PeerChatInterfaceState?, fetchResource: @escaping (Account, MediaResource, Signal<[(Range, MediaBoxFetchPriority)], NoError>, MediaResourceFetchParameters?) -> Signal?, fetchResourceMediaReferenceHash: @escaping (MediaResource) -> Signal) { self.updatePeerChatInputState = updatePeerChatInputState self.fetchResource = fetchResource self.fetchResourceMediaReferenceHash = fetchResourceMediaReferenceHash } } public struct AccountRunningImportantTasks: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { self.rawValue = rawValue } public static let other = AccountRunningImportantTasks(rawValue: 1 << 0) public static let pendingMessages = AccountRunningImportantTasks(rawValue: 1 << 1) } private struct MasterNotificationKey { let id: Data let data: Data } private func masterNotificationsKey(account: Account, ignoreDisabled: Bool) -> Signal { if let key = account.masterNotificationKey.with({ $0 }) { return .single(key) } return account.postbox.transaction(ignoreDisabled: ignoreDisabled, { transaction -> MasterNotificationKey in if let value = transaction.keychainEntryForKey("master-notification-secret"), !value.isEmpty { let authKeyHash = sha1Digest(value) let authKeyId = authKeyHash.subdata(in: authKeyHash.count - 8 ..< authKeyHash.count) let keyData = MasterNotificationKey(id: authKeyId, data: value) let _ = account.masterNotificationKey.swap(keyData) return keyData } else { var secretData = Data(count: 256) let secretDataCount = secretData.count if !secretData.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer) -> Bool in let copyResult = SecRandomCopyBytes(nil, secretDataCount, bytes) return copyResult == errSecSuccess }) { assertionFailure() } transaction.setKeychainEntry(secretData, forKey: "master-notification-secret") let authKeyHash = sha1Digest(secretData) let authKeyId = authKeyHash.subdata(in: authKeyHash.count - 8 ..< authKeyHash.count) let keyData = MasterNotificationKey(id: authKeyId, data: secretData) let _ = account.masterNotificationKey.swap(keyData) return keyData } }) } public func decryptedNotificationPayload(account: Account, data: Data) -> Signal { return masterNotificationsKey(account: account, ignoreDisabled: true) |> map { secret -> Data? in if data.subdata(in: 0 ..< 8) != secret.id { return nil } let x = 8 let msgKey = data.subdata(in: 8 ..< (8 + 16)) let rawData = data.subdata(in: (8 + 16) ..< data.count) let sha256_a = sha256Digest(msgKey + secret.data.subdata(in: x ..< (x + 36))) let sha256_b = sha256Digest(secret.data.subdata(in: (40 + x) ..< (40 + x + 36)) + msgKey) let aesKey = sha256_a.subdata(in: 0 ..< 8) + sha256_b.subdata(in: 8 ..< (8 + 16)) + sha256_a.subdata(in: 24 ..< (24 + 8)) let aesIv = sha256_b.subdata(in: 0 ..< 8) + sha256_a.subdata(in: 8 ..< (8 + 16)) + sha256_b.subdata(in: 24 ..< (24 + 8)) guard let data = MTAesDecrypt(rawData, aesKey, aesIv), data.count > 4 else { return nil } var dataLength: Int32 = 0 data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in memcpy(&dataLength, bytes, 4) } if dataLength < 0 || dataLength > data.count - 4 { return nil } let checkMsgKeyLarge = sha256Digest(secret.data.subdata(in: (88 + x) ..< (88 + x + 32)) + data) let checkMsgKey = checkMsgKeyLarge.subdata(in: 8 ..< (8 + 16)) if checkMsgKey != msgKey { return nil } return data.subdata(in: 4 ..< (4 + Int(dataLength))) } } public class Account { public let id: AccountRecordId public let basePath: String public let testingEnvironment: Bool public let postbox: Postbox public let network: Network let networkArguments: NetworkInitializationArguments public let peerId: PeerId public let auxiliaryMethods: AccountAuxiliaryMethods private let serviceQueue = Queue() public private(set) var stateManager: AccountStateManager! private(set) var contactSyncManager: ContactSyncManager! public private(set) var callSessionManager: CallSessionManager! public private(set) var viewTracker: AccountViewTracker! public private(set) var pendingMessageManager: PendingMessageManager! public private(set) var messageMediaPreuploadManager: MessageMediaPreuploadManager! private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext! private var peerInputActivityManager: PeerInputActivityManager! private var localInputActivityManager: PeerInputActivityManager! private var accountPresenceManager: AccountPresenceManager! private var notificationAutolockReportManager: NotificationAutolockReportManager! fileprivate let managedContactsDisposable = MetaDisposable() fileprivate let managedStickerPacksDisposable = MetaDisposable() private let becomeMasterDisposable = MetaDisposable() private let managedServiceViewsDisposable = MetaDisposable() private let managedOperationsDisposable = DisposableSet() private var storageSettingsDisposable: Disposable? public let graphicsThreadPool = ThreadPool(threadCount: 3, threadPriority: 0.1) public var applicationContext: Any? public let notificationToken = Promise() public let voipToken = Promise() private var notificationTokensVersionValue = 0 { didSet { self.notificationTokensVersionPromise.set(self.notificationTokensVersionValue) } } func updateNotificationTokensVersion() { self.notificationTokensVersionValue += 1 } private let notificationTokensVersionPromise = ValuePromise(0) private let notificationTokenDisposable = MetaDisposable() private let voipTokenDisposable = MetaDisposable() public let importableContacts = Promise<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]>() public let shouldBeServiceTaskMaster = Promise() public let shouldKeepOnlinePresence = Promise() public let autolockReportDeadline = Promise() public let shouldExplicitelyKeepWorkerConnections = Promise(false) private let networkStateValue = Promise(.waitingForNetwork) public var networkState: Signal { return self.networkStateValue.get() } private let networkTypeValue = Promise() public var networkType: Signal { return self.networkTypeValue.get() } private let _loggedOut = ValuePromise(false, ignoreRepeated: true) public var loggedOut: Signal { return self._loggedOut.get() } private let _importantTasksRunning = ValuePromise([], ignoreRepeated: true) public var importantTasksRunning: Signal { return self._importantTasksRunning.get() } fileprivate let masterNotificationKey = Atomic(value: nil) var transformOutgoingMessageMedia: TransformOutgoingMessageMedia? public init(id: AccountRecordId, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, networkArguments: NetworkInitializationArguments, peerId: PeerId, auxiliaryMethods: AccountAuxiliaryMethods) { self.id = id self.basePath = basePath self.testingEnvironment = testingEnvironment self.postbox = postbox self.network = network self.networkArguments = networkArguments self.peerId = peerId self.auxiliaryMethods = auxiliaryMethods self.peerInputActivityManager = PeerInputActivityManager() self.stateManager = AccountStateManager(account: self, peerInputActivityManager: self.peerInputActivityManager, auxiliaryMethods: auxiliaryMethods) self.contactSyncManager = ContactSyncManager(postbox: postbox, network: network, accountPeerId: peerId, stateManager: self.stateManager) self.callSessionManager = CallSessionManager(postbox: postbox, network: network, maxLayer: networkArguments.voipMaxLayer, addUpdates: { [weak self] updates in self?.stateManager.addUpdates(updates) }) self.localInputActivityManager = PeerInputActivityManager() self.accountPresenceManager = AccountPresenceManager(shouldKeepOnlinePresence: self.shouldKeepOnlinePresence.get(), network: network) let _ = (postbox.transaction { transaction -> Void in transaction.updatePeerPresencesInternal(presences: [peerId: TelegramUserPresence(status: .present(until: Int32.max - 1), lastActivity: 0)], merge: { _, updated in return updated }) }).start() self.notificationAutolockReportManager = NotificationAutolockReportManager(deadline: self.autolockReportDeadline.get(), network: network) self.autolockReportDeadline.set( postbox.combinedView(keys: [.accessChallengeData]) |> map { view -> Int32? in guard let dataView = view.views[.accessChallengeData] as? AccessChallengeDataView else { return nil } guard let autolockDeadline = dataView.data.autolockDeadline else { return nil } return autolockDeadline } |> distinctUntilChanged ) self.viewTracker = AccountViewTracker(account: self) self.messageMediaPreuploadManager = MessageMediaPreuploadManager() self.mediaReferenceRevalidationContext = MediaReferenceRevalidationContext() self.pendingMessageManager = PendingMessageManager(network: network, postbox: postbox, accountPeerId: peerId, auxiliaryMethods: auxiliaryMethods, stateManager: self.stateManager, localInputActivityManager: self.localInputActivityManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext) self.network.loggedOut = { [weak self] in Logger.shared.log("Account", "network logged out") if let strongSelf = self { strongSelf._loggedOut.set(true) strongSelf.callSessionManager.dropAll() } } let previousNetworkStatus = Atomic(value: nil) let networkStateQueue = Queue() let delayNetworkStatus = self.shouldBeServiceTaskMaster.get() |> map { mode -> Bool in switch mode { case .now, .always: return true case .never: return false } } |> distinctUntilChanged |> deliverOn(networkStateQueue) |> mapToSignal { value -> Signal in var shouldDelay = false let _ = previousNetworkStatus.modify { previous in if let previous = previous { if !previous && value { shouldDelay = true } } else { shouldDelay = true } return value } if shouldDelay { let delayedFalse = Signal.single(false) |> delay(3.0, queue: networkStateQueue) return .single(true) |> then(delayedFalse) } else { return .single(!value) } } let networkStateSignal = combineLatest(self.stateManager.isUpdating |> deliverOn(networkStateQueue), network.connectionStatus |> deliverOn(networkStateQueue)/*, delayNetworkStatus |> deliverOn(networkStateQueue)*/) |> map { isUpdating, connectionStatus/*, delayNetworkStatus*/ -> AccountNetworkState in /*if delayNetworkStatus { return .online(proxy: nil) }*/ switch connectionStatus { case .waitingForNetwork: return .waitingForNetwork case let .connecting(proxyAddress, proxyHasConnectionIssues): var proxyState: AccountNetworkProxyState? if let proxyAddress = proxyAddress { proxyState = AccountNetworkProxyState(address: proxyAddress, hasConnectionIssues: proxyHasConnectionIssues) } return .connecting(proxy: proxyState) case let .updating(proxyAddress): var proxyState: AccountNetworkProxyState? if let proxyAddress = proxyAddress { proxyState = AccountNetworkProxyState(address: proxyAddress, hasConnectionIssues: false) } return .updating(proxy: proxyState) case let .online(proxyAddress): var proxyState: AccountNetworkProxyState? if let proxyAddress = proxyAddress { proxyState = AccountNetworkProxyState(address: proxyAddress, hasConnectionIssues: false) } if isUpdating { return .updating(proxy: proxyState) } else { return .online(proxy: proxyState) } } } self.networkStateValue.set(networkStateSignal |> distinctUntilChanged) self.networkTypeValue.set(currentNetworkType()) let appliedNotificationToken = combineLatest(self.notificationToken.get(), self.notificationTokensVersionPromise.get()) |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token, _ -> Signal in var tokenString = "" token.withUnsafeBytes { (bytes: UnsafePointer) -> Void in for i in 0 ..< token.count { let byte = bytes.advanced(by: i).pointee tokenString = tokenString.appendingFormat("%02x", Int32(byte)) } } var appSandbox: Api.Bool = .boolFalse #if DEBUG appSandbox = .boolTrue #endif return masterNotificationsKey(account: self, ignoreDisabled: false) |> mapToSignal { secret -> Signal in return network.request(Api.functions.account.registerDevice(tokenType: 1, token: tokenString, appSandbox: appSandbox, secret: Buffer(/*data: secret.data*/), otherUids: [])) |> retryRequest |> mapToSignal { _ -> Signal in return .complete() } } } self.notificationTokenDisposable.set(appliedNotificationToken.start()) let appliedVoipToken = combineLatest(self.voipToken.get(), self.notificationTokensVersionPromise.get()) |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token, _ -> Signal in var tokenString = "" token.withUnsafeBytes { (bytes: UnsafePointer) -> Void in for i in 0 ..< token.count { let byte = bytes.advanced(by: i).pointee tokenString = tokenString.appendingFormat("%02x", Int32(byte)) } } var appSandbox: Api.Bool = .boolFalse #if DEBUG appSandbox = .boolTrue #endif return masterNotificationsKey(account: self, ignoreDisabled: false) |> mapToSignal { secret -> Signal in return network.request(Api.functions.account.registerDevice(tokenType: 9, token: tokenString, appSandbox: appSandbox, secret: Buffer(data: secret.data), otherUids: [])) |> retryRequest |> mapToSignal { _ -> Signal in return .complete() } } } self.voipTokenDisposable.set(appliedVoipToken.start()) let serviceTasksMasterBecomeMaster = shouldBeServiceTaskMaster.get() |> distinctUntilChanged |> deliverOn(self.serviceQueue) self.becomeMasterDisposable.set(serviceTasksMasterBecomeMaster.start(next: { [weak self] value in if let strongSelf = self, (value == .now || value == .always) { strongSelf.postbox.becomeMasterClient() } })) let shouldBeMaster = combineLatest(shouldBeServiceTaskMaster.get(), postbox.isMasterClient()) |> map { [weak self] shouldBeMaster, isMaster -> Bool in if shouldBeMaster == .always && !isMaster { self?.postbox.becomeMasterClient() } return (shouldBeMaster == .now || shouldBeMaster == .always) && isMaster } |> distinctUntilChanged self.network.shouldKeepConnection.set(shouldBeMaster) self.network.shouldExplicitelyKeepWorkerConnections.set(self.shouldExplicitelyKeepWorkerConnections.get()) let serviceTasksMaster = shouldBeMaster |> deliverOn(self.serviceQueue) |> mapToSignal { [weak self] value -> Signal in if let strongSelf = self, value { Logger.shared.log("Account", "Became master") return managedServiceViews(accountPeerId: peerId, network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, pendingMessageManager: strongSelf.pendingMessageManager) } else { Logger.shared.log("Account", "Resigned master") return .never() } } self.managedServiceViewsDisposable.set(serviceTasksMaster.start()) let pendingMessageManager = self.pendingMessageManager self.managedOperationsDisposable.add(postbox.unsentMessageIdsView().start(next: { [weak pendingMessageManager] view in pendingMessageManager?.updatePendingMessageIds(view.ids) })) self.managedOperationsDisposable.add(managedSecretChatOutgoingOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedGlobalNotificationSettings(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizePinnedChatsOperations(postbox: self.postbox, network: self.network, accountPeerId: self.peerId, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeGroupedPeersOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .stickers).start()) self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .masks).start()) self.managedOperationsDisposable.add(managedSynchronizeMarkFeaturedStickerPacksAsSeenOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizeRecentlyUsedMediaOperations(postbox: self.postbox, network: self.network, category: .stickers, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedSynchronizeSavedGifsOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedSynchronizeSavedStickersOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedRecentlyUsedInlineBots(postbox: self.postbox, network: self.network, accountPeerId: peerId).start()) self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.postbox, network: self.network, accountPeerId: self.peerId).start()) self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) let importantBackgroundOperations: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, self.pendingMessageManager.hasPendingMessages |> map { $0 ? AccountRunningImportantTasks.pendingMessages : [] }, self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] }, self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] } ] let importantBackgroundOperationsRunning = combineLatest(importantBackgroundOperations) |> deliverOn(Queue()) |> map { values -> AccountRunningImportantTasks in var result: AccountRunningImportantTasks = [] for value in values { result.formUnion(value) } return result } self.managedOperationsDisposable.add(importantBackgroundOperationsRunning.start(next: { [weak self] value in if let strongSelf = self { strongSelf._importantTasksRunning.set(value) } })) self.managedOperationsDisposable.add(managedConfigurationUpdates(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedVoipConfigurationUpdates(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedAppConfigurationUpdates(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedTermsOfServiceUpdates(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedAppChangelog(postbox: self.postbox, network: self.network, stateManager: self.stateManager, appVersion: self.networkArguments.appVersion).start()) self.managedOperationsDisposable.add(managedProxyInfoUpdates(postbox: self.postbox, network: self.network, viewTracker: self.viewTracker).start()) self.managedOperationsDisposable.add(managedLocalizationUpdatesOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedPendingPeerNotificationSettings(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizeAppLogEventsOperations(postbox: self.postbox, network: self.network).start()) let storagePreferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.cacheStorageSettings])) let mediaBox = postbox.mediaBox self.storageSettingsDisposable = self.postbox.combinedView(keys: [storagePreferencesKey]).start(next: { [weak mediaBox] view in guard let mediaBox = mediaBox else { return } let settings: CacheStorageSettings = ((view.views[storagePreferencesKey] as? PreferencesView)?.values[PreferencesKeys.cacheStorageSettings] as? CacheStorageSettings) ?? CacheStorageSettings.defaultSettings mediaBox.setMaxStoreTime(settings.defaultCacheStorageTimeout) }) } deinit { self.managedContactsDisposable.dispose() self.managedStickerPacksDisposable.dispose() self.notificationTokenDisposable.dispose() self.voipTokenDisposable.dispose() self.managedServiceViewsDisposable.dispose() self.managedOperationsDisposable.dispose() self.storageSettingsDisposable?.dispose() } public func resetStateManagement() { self.stateManager.reset() self.contactSyncManager.beginSync(importableContacts: self.importableContacts.get()) self.managedStickerPacksDisposable.set(manageStickerPacks(network: self.network, postbox: self.postbox).start()) } public func peerInputActivities(peerId: PeerId) -> Signal<[(PeerId, PeerInputActivity)], NoError> { return self.peerInputActivityManager.activities(peerId: peerId) |> map { activities in return activities.map({ ($0.0, $0.1.activity) }) } } public func allPeerInputActivities() -> Signal<[PeerId: [PeerId: PeerInputActivity]], NoError> { return self.peerInputActivityManager.allActivities() |> map { activities in var result: [PeerId: [PeerId: PeerInputActivity]] = [:] for (chatPeerId, chatActivities) in activities { result[chatPeerId] = chatActivities.mapValues({ $0.activity }) } return result } } public func updateLocalInputActivity(peerId: PeerId, activity: PeerInputActivity, isPresent: Bool) { self.localInputActivityManager.transaction { manager in if isPresent { manager.addActivity(chatPeerId: peerId, peerId: self.peerId, activity: activity) } else { manager.removeActivity(chatPeerId: peerId, peerId: self.peerId, activity: activity) } } } public func acquireLocalInputActivity(peerId: PeerId, activity: PeerInputActivity) -> Disposable { return self.localInputActivityManager.acquireActivity(chatPeerId: peerId, peerId: self.peerId, activity: activity) } } public func accountNetworkUsageStats(account: Account, reset: ResetNetworkUsageStats) -> Signal { return networkUsageStats(basePath: account.basePath, reset: reset) } public func updateAccountNetworkUsageStats(account: Account, category: MediaResourceStatsCategory, delta: NetworkUsageStatsConnectionsEntry) { updateNetworkUsageStats(basePath: account.basePath, category: category, delta: delta) } public typealias FetchCachedResourceRepresentation = (_ account: Account, _ resource: MediaResource, _ representation: CachedMediaResourceRepresentation) -> Signal public typealias TransformOutgoingMessageMedia = (_ postbox: Postbox, _ network: Network, _ media: AnyMediaReference, _ userInteractive: Bool) -> Signal public func setupAccount(_ account: Account, fetchCachedResourceRepresentation: FetchCachedResourceRepresentation? = nil, transformOutgoingMessageMedia: TransformOutgoingMessageMedia? = nil, preFetchedResourcePath: @escaping (MediaResource) -> String? = { _ in return nil }) { account.postbox.mediaBox.preFetchedResourcePath = preFetchedResourcePath account.postbox.mediaBox.fetchResource = { [weak account] resource, intervals, parameters -> Signal in if let strongAccount = account { if let result = fetchResource(account: strongAccount, resource: resource, intervals: intervals, parameters: parameters) { return result } else if let result = strongAccount.auxiliaryMethods.fetchResource(strongAccount, resource, intervals, parameters) { return result } else { return .never() } } else { return .never() } } account.postbox.mediaBox.fetchCachedResourceRepresentation = { [weak account] resource, representation in if let strongAccount = account, let fetchCachedResourceRepresentation = fetchCachedResourceRepresentation { return fetchCachedResourceRepresentation(strongAccount, resource, representation) } else { return .never() } } account.transformOutgoingMessageMedia = transformOutgoingMessageMedia account.pendingMessageManager.transformOutgoingMessageMedia = transformOutgoingMessageMedia }