mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Bot auth
This commit is contained in:
parent
4cef442e92
commit
af6f1da0ab
@ -705,6 +705,8 @@ private final class NotificationServiceHandler {
|
|||||||
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
setupSharedLogger(rootPath: logsPath, path: logsPath)
|
setupSharedLogger(rootPath: logsPath, path: logsPath)
|
||||||
|
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Started handling notification")
|
||||||
|
|
||||||
initializeAccountManagement()
|
initializeAccountManagement()
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import Security
|
||||||
|
|
||||||
public enum LocalAuthBiometricAuthentication {
|
public enum LocalAuthBiometricAuthentication {
|
||||||
case touchId
|
case touchId
|
||||||
@ -8,10 +9,65 @@ public enum LocalAuthBiometricAuthentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct LocalAuth {
|
public struct LocalAuth {
|
||||||
public static let biometricAuthentication: LocalAuthBiometricAuthentication? = {
|
private static let customKeyIdPrefix = "$#_".data(using: .utf8)!
|
||||||
|
|
||||||
|
public enum DecryptionResult {
|
||||||
|
public enum Error {
|
||||||
|
case cancelled
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
case result(Data)
|
||||||
|
case error(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class PrivateKey {
|
||||||
|
private let privateKey: SecKey
|
||||||
|
private let publicKey: SecKey
|
||||||
|
public let publicKeyRepresentation: Data
|
||||||
|
|
||||||
|
fileprivate init(privateKey: SecKey, publicKey: SecKey, publicKeyRepresentation: Data) {
|
||||||
|
self.privateKey = privateKey
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.publicKeyRepresentation = publicKeyRepresentation
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encrypt(data: Data) -> Data? {
|
||||||
|
var error: Unmanaged<CFError>?
|
||||||
|
let cipherText = SecKeyCreateEncryptedData(self.publicKey, .eciesEncryptionCofactorVariableIVX963SHA512AESGCM, data as CFData, &error)
|
||||||
|
if let error {
|
||||||
|
error.release()
|
||||||
|
}
|
||||||
|
guard let cipherText else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = cipherText as Data
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func decrypt(data: Data) -> DecryptionResult {
|
||||||
|
var maybeError: Unmanaged<CFError>?
|
||||||
|
let plainText = SecKeyCreateDecryptedData(self.privateKey, .eciesEncryptionCofactorVariableIVX963SHA512AESGCM, data as CFData, &maybeError)
|
||||||
|
let error = maybeError?.takeRetainedValue()
|
||||||
|
|
||||||
|
guard let plainText else {
|
||||||
|
if let error {
|
||||||
|
if CFErrorGetCode(error) == -2 {
|
||||||
|
return .error(.cancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .error(.generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = plainText as Data
|
||||||
|
return .result(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var biometricAuthentication: LocalAuthBiometricAuthentication? {
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
if context.canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) {
|
if context.canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) {
|
||||||
#if swift(>=5.9)
|
|
||||||
switch context.biometryType {
|
switch context.biometryType {
|
||||||
case .faceID, .opticID:
|
case .faceID, .opticID:
|
||||||
return .faceId
|
return .faceId
|
||||||
@ -22,22 +78,10 @@ public struct LocalAuth {
|
|||||||
@unknown default:
|
@unknown default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
switch context.biometryType {
|
|
||||||
case .faceID://, .opticID:
|
|
||||||
return .faceId
|
|
||||||
case .touchID:
|
|
||||||
return .touchId
|
|
||||||
case .none:
|
|
||||||
return nil
|
|
||||||
@unknown default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
public static let evaluatedPolicyDomainState: Data? = {
|
public static let evaluatedPolicyDomainState: Data? = {
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
@ -78,4 +122,145 @@ public struct LocalAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func bundleSeedId() -> String? {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword as String,
|
||||||
|
kSecAttrAccount as String: "bundleSeedID",
|
||||||
|
kSecAttrService as String: "",
|
||||||
|
kSecReturnAttributes as String: true
|
||||||
|
]
|
||||||
|
var result: CFTypeRef?
|
||||||
|
var status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
if status == errSecItemNotFound {
|
||||||
|
status = SecItemAdd(query as CFDictionary, &result)
|
||||||
|
}
|
||||||
|
if status != errSecSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let result = result else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if CFGetTypeID(result) != CFDictionaryGetTypeID() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let resultDict = (result as! CFDictionary) as? [String: Any] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let components = accessGroup.components(separatedBy: ".")
|
||||||
|
guard let seedId = components.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return seedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
|
||||||
|
guard let bundleSeedId = self.bundleSeedId() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let applicationTag = customKeyIdPrefix + keyId
|
||||||
|
let accessGroup = "\(bundleSeedId).\(baseAppBundleId)"
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassKey as String,
|
||||||
|
kSecAttrApplicationTag as String: applicationTag,
|
||||||
|
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String,
|
||||||
|
kSecAttrAccessGroup as String: accessGroup,
|
||||||
|
kSecReturnRef as String: true
|
||||||
|
]
|
||||||
|
|
||||||
|
var maybePrivateKey: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &maybePrivateKey)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let maybePrivateKey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if CFGetTypeID(maybePrivateKey) != SecKeyGetTypeID() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let privateKey = maybePrivateKey as! SecKey
|
||||||
|
|
||||||
|
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let publicKeyRepresentation = SecKeyCopyExternalRepresentation(publicKey, nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func removePrivateKey(baseAppBundleId: String, keyId: Data) -> Bool {
|
||||||
|
guard let bundleSeedId = self.bundleSeedId() else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let applicationTag = customKeyIdPrefix + keyId
|
||||||
|
let accessGroup = "\(bundleSeedId).\(baseAppBundleId)"
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassKey as String,
|
||||||
|
kSecAttrApplicationTag as String: applicationTag,
|
||||||
|
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String,
|
||||||
|
kSecAttrIsPermanent as String: true,
|
||||||
|
kSecAttrAccessGroup as String: accessGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func addPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
|
||||||
|
guard let bundleSeedId = self.bundleSeedId() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let applicationTag = customKeyIdPrefix + keyId
|
||||||
|
let accessGroup = "\(bundleSeedId).\(baseAppBundleId)"
|
||||||
|
|
||||||
|
guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.userPresence, .privateKeyUsage], nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributes: [String: Any] = [
|
||||||
|
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String,
|
||||||
|
kSecAttrKeySizeInBits as String: 256 as NSNumber,
|
||||||
|
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave as String,
|
||||||
|
kSecPrivateKeyAttrs as String: [
|
||||||
|
kSecAttrIsPermanent as String: true,
|
||||||
|
kSecAttrApplicationTag as String: applicationTag,
|
||||||
|
kSecAttrAccessControl as String: access,
|
||||||
|
kSecAttrAccessGroup as String: accessGroup,
|
||||||
|
] as [String: Any]
|
||||||
|
]
|
||||||
|
var error: Unmanaged<CFError>?
|
||||||
|
let maybePrivateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
|
||||||
|
if let error {
|
||||||
|
error.release()
|
||||||
|
}
|
||||||
|
guard let privateKey = maybePrivateKey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let publicKeyRepresentation = SecKeyCopyExternalRepresentation(publicKey, nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,6 +286,7 @@ private enum PreferencesKeyValues: Int32 {
|
|||||||
case displaySavedChatsAsTopics = 35
|
case displaySavedChatsAsTopics = 35
|
||||||
case shortcutMessages = 37
|
case shortcutMessages = 37
|
||||||
case timezoneList = 38
|
case timezoneList = 38
|
||||||
|
case botBiometricsState = 39
|
||||||
}
|
}
|
||||||
|
|
||||||
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
|
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
|
||||||
@ -481,6 +482,13 @@ public struct PreferencesKeys {
|
|||||||
key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue)
|
key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue)
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func botBiometricsState(peerId: PeerId) -> ValueBoxKey {
|
||||||
|
let key = ValueBoxKey(length: 4 + 8)
|
||||||
|
key.setInt32(0, value: PreferencesKeyValues.botBiometricsState.rawValue)
|
||||||
|
key.setInt64(4, value: peerId.toInt64())
|
||||||
|
return key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum SharedDataKeyValues: Int32 {
|
private enum SharedDataKeyValues: Int32 {
|
||||||
|
@ -1584,5 +1584,33 @@ public extension TelegramEngine.EngineData.Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct BotBiometricsState: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||||
|
public typealias Result = TelegramBotBiometricsState
|
||||||
|
|
||||||
|
fileprivate var id: EnginePeer.Id
|
||||||
|
public var mapKey: EnginePeer.Id {
|
||||||
|
return self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(id: EnginePeer.Id) {
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
var key: PostboxViewKey {
|
||||||
|
return .preferences(keys: Set([PreferencesKeys.botBiometricsState(peerId: self.id)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func extract(view: PostboxView) -> Result {
|
||||||
|
guard let view = view as? PreferencesView else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
if let state = view.values[PreferencesKeys.botBiometricsState(peerId: self.id)]?.get(TelegramBotBiometricsState.self) {
|
||||||
|
return state
|
||||||
|
} else {
|
||||||
|
return TelegramBotBiometricsState.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,3 +319,42 @@ func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId:
|
|||||||
|> castError(InvokeBotCustomMethodError.self)
|
|> castError(InvokeBotCustomMethodError.self)
|
||||||
|> switchToLatest
|
|> switchToLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct TelegramBotBiometricsState: Codable, Equatable {
|
||||||
|
public struct OpaqueToken: Codable, Equatable {
|
||||||
|
public let publicKey: Data
|
||||||
|
public let data: Data
|
||||||
|
|
||||||
|
public init(publicKey: Data, data: Data) {
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var accessRequested: Bool
|
||||||
|
public var accessGranted: Bool
|
||||||
|
public var opaqueToken: OpaqueToken?
|
||||||
|
|
||||||
|
public static var `default`: TelegramBotBiometricsState {
|
||||||
|
return TelegramBotBiometricsState(
|
||||||
|
accessRequested: false,
|
||||||
|
accessGranted: false,
|
||||||
|
opaqueToken: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(accessRequested: Bool, accessGranted: Bool, opaqueToken: OpaqueToken?) {
|
||||||
|
self.accessRequested = accessRequested
|
||||||
|
self.accessGranted = accessGranted
|
||||||
|
self.opaqueToken = opaqueToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) -> Signal<Never, NoError> {
|
||||||
|
return account.postbox.transaction { transaction -> Void in
|
||||||
|
let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId))?.get(TelegramBotBiometricsState.self) ?? TelegramBotBiometricsState.default
|
||||||
|
|
||||||
|
transaction.setPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId), value: PreferencesEntry(update(previousState)))
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
@ -1490,6 +1490,10 @@ public extension TelegramEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateBotBiometricsState(peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) {
|
||||||
|
let _ = _internal_updateBotBiometricsState(account: self.account, peerId: peerId, update: update).startStandalone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ swift_library(
|
|||||||
"//submodules/CheckNode:CheckNode",
|
"//submodules/CheckNode:CheckNode",
|
||||||
"//submodules/Markdown:Markdown",
|
"//submodules/Markdown:Markdown",
|
||||||
"//submodules/TextFormat:TextFormat",
|
"//submodules/TextFormat:TextFormat",
|
||||||
|
"//submodules/LocalAuth",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -23,6 +23,7 @@ import PromptUI
|
|||||||
import PhoneNumberFormat
|
import PhoneNumberFormat
|
||||||
import QrCodeUI
|
import QrCodeUI
|
||||||
import InstantPageUI
|
import InstantPageUI
|
||||||
|
import LocalAuth
|
||||||
|
|
||||||
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
||||||
|
|
||||||
@ -1064,6 +1065,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
if let json = json, let isVisible = json["is_visible"] as? Bool {
|
if let json = json, let isVisible = json["is_visible"] as? Bool {
|
||||||
self.controller?.hasSettings = isVisible
|
self.controller?.hasSettings = isVisible
|
||||||
}
|
}
|
||||||
|
case "web_app_biometry_get_info":
|
||||||
|
self.sendBiometryInfoReceivedEvent()
|
||||||
|
case "web_app_biometry_request_access":
|
||||||
|
self.requestBiometryAccess()
|
||||||
|
case "web_app_biometry_request_auth":
|
||||||
|
self.requestBiometryAuth()
|
||||||
|
case "web_app_biometry_update_token":
|
||||||
|
var tokenData: Data?
|
||||||
|
if let json, let tokenDataValue = json["token"] as? Data {
|
||||||
|
tokenData = tokenDataValue
|
||||||
|
}
|
||||||
|
self.requestBiometryUpdateToken(tokenData: tokenData)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -1392,6 +1405,230 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString)
|
self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func sendBiometryInfoReceivedEvent() {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: [String: Any] = [:]
|
||||||
|
if let biometricAuthentication = LocalAuth.biometricAuthentication {
|
||||||
|
data["available"] = true
|
||||||
|
switch biometricAuthentication {
|
||||||
|
case .faceId:
|
||||||
|
data["type"] = "face"
|
||||||
|
case .touchId:
|
||||||
|
data["type"] = "finger"
|
||||||
|
}
|
||||||
|
data["access_requested"] = state.accessRequested
|
||||||
|
data["access_granted"] = state.accessGranted
|
||||||
|
data["token_saved"] = state.opaqueToken != nil
|
||||||
|
} else {
|
||||||
|
data["available"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.webView?.sendEvent(name: "biometry_info_received", data: jsonDataString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func requestBiometryAccess() {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] botPeer, state in
|
||||||
|
guard let self, let botPeer, let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.accessRequested {
|
||||||
|
self.sendBiometryInfoReceivedEvent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateAccessGranted: (Bool) -> Void = { [weak self] granted in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context.engine.peers.updateBotBiometricsState(peerId: botPeer.id, update: { state in
|
||||||
|
var state = state
|
||||||
|
state.accessRequested = true
|
||||||
|
state.accessGranted = granted
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
|
||||||
|
self.sendBiometryInfoReceivedEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let alertText = "Do you want to allow \(botPeer.compactDisplayTitle) to use Face ID?"
|
||||||
|
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: alertText, actions: [
|
||||||
|
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: {
|
||||||
|
updateAccessGranted(false)
|
||||||
|
}),
|
||||||
|
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Yes, action: {
|
||||||
|
updateAccessGranted(true)
|
||||||
|
})
|
||||||
|
], parseMarkdown: false), in: .window(.root))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func requestBiometryAuth() {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] botPeer, state in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.accessRequested && state.accessGranted {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let keyId = "A\(UInt64(bitPattern: self.context.account.id.int64))WebBot\(UInt64(bitPattern: controller.botId.toInt64()))".data(using: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let appBundleId = self.context.sharedContext.applicationBindings.appBundleId
|
||||||
|
|
||||||
|
Thread { [weak self] in
|
||||||
|
var key = LocalAuth.getPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
|
||||||
|
if key == nil {
|
||||||
|
key = LocalAuth.addPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let decryptedData: LocalAuth.DecryptionResult
|
||||||
|
if let key {
|
||||||
|
if let encryptedData = state.opaqueToken {
|
||||||
|
if encryptedData.publicKey == key.publicKeyRepresentation {
|
||||||
|
decryptedData = key.decrypt(data: encryptedData.data)
|
||||||
|
} else {
|
||||||
|
// The local keychain has been reset
|
||||||
|
if let emptyEncryptedData = key.encrypt(data: Data()) {
|
||||||
|
decryptedData = key.decrypt(data: emptyEncryptedData)
|
||||||
|
} else {
|
||||||
|
decryptedData = .error(.generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let emptyEncryptedData = key.encrypt(data: Data()) {
|
||||||
|
decryptedData = key.decrypt(data: emptyEncryptedData)
|
||||||
|
} else {
|
||||||
|
decryptedData = .error(.generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedData = .error(.generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch decryptedData {
|
||||||
|
case let .result(token):
|
||||||
|
self.sendBiometryAuthResult(isAuthorized: true, tokenData: state.opaqueToken != nil ? token : nil)
|
||||||
|
case .error:
|
||||||
|
self.sendBiometryAuthResult(isAuthorized: false, tokenData: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
} else {
|
||||||
|
self.sendBiometryAuthResult(isAuthorized: false, tokenData: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func sendBiometryAuthResult(isAuthorized: Bool, tokenData: Data?) {
|
||||||
|
var data: [String: Any] = [:]
|
||||||
|
data["status"] = isAuthorized ? "authorized" : "failed"
|
||||||
|
if isAuthorized {
|
||||||
|
if let tokenData {
|
||||||
|
data["token"] = tokenData
|
||||||
|
} else {
|
||||||
|
data["token"] = Data()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.webView?.sendEvent(name: "biometry_auth_requested", data: jsonDataString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func requestBiometryUpdateToken(tokenData: Data?) {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let keyId = "A\(UInt64(bitPattern: self.context.account.id.int64))WebBot\(UInt64(bitPattern: controller.botId.toInt64()))".data(using: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let tokenData {
|
||||||
|
let appBundleId = self.context.sharedContext.applicationBindings.appBundleId
|
||||||
|
Thread { [weak self] in
|
||||||
|
var key = LocalAuth.getPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
|
||||||
|
if key == nil {
|
||||||
|
key = LocalAuth.addPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptedData: TelegramBotBiometricsState.OpaqueToken?
|
||||||
|
if let key {
|
||||||
|
if let result = key.encrypt(data: tokenData) {
|
||||||
|
encryptedData = TelegramBotBiometricsState.OpaqueToken(
|
||||||
|
publicKey: key.publicKeyRepresentation,
|
||||||
|
data: result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let encryptedData {
|
||||||
|
self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in
|
||||||
|
var state = state
|
||||||
|
state.opaqueToken = encryptedData
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
} else {
|
||||||
|
self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in
|
||||||
|
var state = state
|
||||||
|
state.opaqueToken = nil
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var controllerNode: Node {
|
fileprivate var controllerNode: Node {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user