From b7380fd910c8c32934a9a35a76674d5ec2411437 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Mon, 8 Jan 2024 14:00:42 +0000 Subject: [PATCH 1/2] WIP: - attempt to use new API version with signature. Currently returning false, needs API change --- .../xcshareddata/swiftpm/Package.resolved | 6 ++-- Kukai Mobile/Services/DiscoverService.swift | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d02394c..32106f59 100644 --- a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -78,7 +78,7 @@ "location" : "https://github.com/kukai-wallet/kukai-core-swift", "state" : { "branch" : "develop", - "revision" : "f4a08fb37302b01cd3cf48cd998fb28cbe39e1a7" + "revision" : "8d08cd56344c34680ead36f5ac0c3fc737ef9930" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kukai-wallet/kukai-crypto-swift", "state" : { - "revision" : "4087e7251d43c73f97f3b572b1006be6fe0e1f7e", - "version" : "1.0.17" + "revision" : "b83b7109b577337600cc064cb95aa1df9f8a9979", + "version" : "1.0.18" } }, { diff --git a/Kukai Mobile/Services/DiscoverService.swift b/Kukai Mobile/Services/DiscoverService.swift index 4d1c4a4d..617281ef 100644 --- a/Kukai Mobile/Services/DiscoverService.swift +++ b/Kukai Mobile/Services/DiscoverService.swift @@ -7,6 +7,13 @@ import Foundation import KukaiCoreSwift +import KukaiCryptoSwift +import OSLog + +public struct DiscoverSecureServiceObject: Codable { + let data: [DiscoverGroup] + let signature: String +} public struct DiscoverGroup: Codable, Hashable, Identifiable { @DefaultUUID public var id: UUID @@ -28,7 +35,7 @@ public struct DiscoverItem: Codable, Hashable, Identifiable { public class DiscoverService { - private let discoverURL = "https://services.kukaiwallet.workers.dev/v1/discover" + private let discoverURL = "https://services.kukai.app/v2/discover" private let discoverCacheKey = "discover-cache-key" @@ -54,13 +61,17 @@ public class DiscoverService { } // Request from API, no more frequently than once per day, else read cache - self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: [DiscoverGroup].self) { [weak self] result in + self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: DiscoverSecureServiceObject.self) { [weak self] result in guard let response = try? result.get() else { completion(Result.failure(result.getFailure())) return } - self?.items = response + self?.items = response.data + + let _ = self?.validate(secureObject: response) + self?.deleteCache() + completion(Result.success(true)) } } @@ -68,4 +79,22 @@ public class DiscoverService { public func deleteCache() { let _ = self.requestIfService.delete(key: discoverCacheKey) } + + private func validate(secureObject: DiscoverSecureServiceObject) -> Bool { + let encoder = JSONEncoder() + + guard let jsonData = try? encoder.encode(secureObject.data), + let publicKeyData = try? Data(hexString: "d71729958d14ba994b9bf29816f9710bd944d0ed7dc3e5a58a31532ca87e06f6"), + let signatureData = try? Data(hexString: secureObject.signature) + else { + Logger.app.error("DiscoverService unable to setup validation data") + return false + } + + let publicKey = PublicKey(publicKeyData.bytes, signingCurve: .ed25519) + let valid = publicKey.verify(message: jsonData.bytes, signature: signatureData.bytes) + + print("valid: \(valid)") + return valid + } } From 46b0fc0d5c328d318e98f5a7f44d5aff26936ee6 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Fri, 12 Jan 2024 12:13:45 +0000 Subject: [PATCH 2/2] - update to v2 of discover and explore - add ability for requestIfService to process secure endpoints, which only write to disk if signature is validated - handle exploreService supporting both mainnet and ghostnet --- Kukai Mobile/Services/DependencyManager.swift | 4 +- Kukai Mobile/Services/DiscoverService.swift | 33 +----- Kukai Mobile/Services/ExploreService.swift | 110 +++++++++++------- Kukai Mobile/Services/RequestIfService.swift | 71 +++++++++-- 4 files changed, 136 insertions(+), 82 deletions(-) diff --git a/Kukai Mobile/Services/DependencyManager.swift b/Kukai Mobile/Services/DependencyManager.swift index 13aa278e..b64b51f1 100644 --- a/Kukai Mobile/Services/DependencyManager.swift +++ b/Kukai Mobile/Services/DependencyManager.swift @@ -197,7 +197,7 @@ class DependencyManager { activityService = ActivityService() coinGeckoService = CoinGeckoService(networkService: tezosNodeClient.networkService) tezosDomainsClient = TezosDomainsClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig) - exploreService = ExploreService(networkService: tezosNodeClient.networkService) + exploreService = ExploreService(networkService: tezosNodeClient.networkService, networkType: .mainnet) discoverService = DiscoverService(networkService: tezosNodeClient.networkService) appUpdateService = AppUpdateService(networkService: tezosNodeClient.networkService) @@ -246,7 +246,7 @@ class DependencyManager { tzktClient = TzKTClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig, betterCallDevClient: betterCallDevClient, dipDupClient: dipDupClient) coinGeckoService = CoinGeckoService(networkService: tezosNodeClient.networkService) tezosDomainsClient = TezosDomainsClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig) - exploreService = ExploreService(networkService: tezosNodeClient.networkService) + exploreService = ExploreService(networkService: tezosNodeClient.networkService, networkType: currentNetworkType) discoverService = DiscoverService(networkService: tezosNodeClient.networkService) appUpdateService = AppUpdateService(networkService: tezosNodeClient.networkService) diff --git a/Kukai Mobile/Services/DiscoverService.swift b/Kukai Mobile/Services/DiscoverService.swift index 617281ef..a6ef8c84 100644 --- a/Kukai Mobile/Services/DiscoverService.swift +++ b/Kukai Mobile/Services/DiscoverService.swift @@ -10,11 +10,6 @@ import KukaiCoreSwift import KukaiCryptoSwift import OSLog -public struct DiscoverSecureServiceObject: Codable { - let data: [DiscoverGroup] - let signature: String -} - public struct DiscoverGroup: Codable, Hashable, Identifiable { @DefaultUUID public var id: UUID @@ -35,7 +30,7 @@ public struct DiscoverItem: Codable, Hashable, Identifiable { public class DiscoverService { - private let discoverURL = "https://services.kukai.app/v2/discover" + private let discoverURL = "https://services.kukai.app/v2/discover?encode=true" private let discoverCacheKey = "discover-cache-key" @@ -61,17 +56,13 @@ public class DiscoverService { } // Request from API, no more frequently than once per day, else read cache - self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: DiscoverSecureServiceObject.self) { [weak self] result in + self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: [DiscoverGroup].self, isSecure: true) { [weak self] result in guard let response = try? result.get() else { completion(Result.failure(result.getFailure())) return } - self?.items = response.data - - let _ = self?.validate(secureObject: response) - self?.deleteCache() - + self?.items = response completion(Result.success(true)) } } @@ -79,22 +70,4 @@ public class DiscoverService { public func deleteCache() { let _ = self.requestIfService.delete(key: discoverCacheKey) } - - private func validate(secureObject: DiscoverSecureServiceObject) -> Bool { - let encoder = JSONEncoder() - - guard let jsonData = try? encoder.encode(secureObject.data), - let publicKeyData = try? Data(hexString: "d71729958d14ba994b9bf29816f9710bd944d0ed7dc3e5a58a31532ca87e06f6"), - let signatureData = try? Data(hexString: secureObject.signature) - else { - Logger.app.error("DiscoverService unable to setup validation data") - return false - } - - let publicKey = PublicKey(publicKeyData.bytes, signingCurve: .ed25519) - let valid = publicKey.verify(message: jsonData.bytes, signature: signatureData.bytes) - - print("valid: \(valid)") - return valid - } } diff --git a/Kukai Mobile/Services/ExploreService.swift b/Kukai Mobile/Services/ExploreService.swift index 30ce0574..9a916d49 100644 --- a/Kukai Mobile/Services/ExploreService.swift +++ b/Kukai Mobile/Services/ExploreService.swift @@ -8,40 +8,20 @@ import Foundation import KukaiCoreSwift -public enum ShouldDisplayLink: String, Codable { - case all - case none + +public struct ExploreResponse: Codable { + let environment: ExploreEnvironments } -public struct RemoteDiscoverItem: Codable { - let dappUrl: URL? - let category: [String]? - let discoverImageUrl: URL? - let hasZoomDiscoverImage: Bool? - - enum CodingKeys: String, CodingKey { - case dappUrl - case category - case discoverImageUrl - case hasZoomDiscoverImage - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - if let urlString = try container.decodeIfPresent(String.self, forKey: .dappUrl) { dappUrl = URL(string: urlString) } else { dappUrl = nil } - if let urlString = try container.decodeIfPresent(String.self, forKey: .discoverImageUrl) { discoverImageUrl = URL(string: urlString) } else { discoverImageUrl = nil } - category = try container.decodeIfPresent([String].self, forKey: .category) - hasZoomDiscoverImage = try container.decodeIfPresent(Bool.self, forKey: .hasZoomDiscoverImage) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(dappUrl?.absoluteString, forKey: .dappUrl) - try container.encode(category, forKey: .category) - try container.encode(discoverImageUrl?.absoluteString, forKey: .discoverImageUrl) - try container.encode(hasZoomDiscoverImage, forKey: .hasZoomDiscoverImage) - } +public struct ExploreEnvironments: Codable { + let mainnet: ExploreEnvironment + let ghostnet: ExploreEnvironment +} + +public struct ExploreEnvironment: Codable { + let blockList: [String] + let model3DAllowList: [String] + let contractAliases: [ExploreItem] } public struct ExploreItem: Codable { @@ -93,24 +73,61 @@ public struct ExploreItem: Codable { } } +public struct RemoteDiscoverItem: Codable { + let dappUrl: URL? + let category: [String]? + let discoverImageUrl: URL? + let hasZoomDiscoverImage: Bool? + + enum CodingKeys: String, CodingKey { + case dappUrl + case category + case discoverImageUrl + case hasZoomDiscoverImage + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let urlString = try container.decodeIfPresent(String.self, forKey: .dappUrl) { dappUrl = URL(string: urlString) } else { dappUrl = nil } + if let urlString = try container.decodeIfPresent(String.self, forKey: .discoverImageUrl) { discoverImageUrl = URL(string: urlString) } else { discoverImageUrl = nil } + category = try container.decodeIfPresent([String].self, forKey: .category) + hasZoomDiscoverImage = try container.decodeIfPresent(Bool.self, forKey: .hasZoomDiscoverImage) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(dappUrl?.absoluteString, forKey: .dappUrl) + try container.encode(category, forKey: .category) + try container.encode(discoverImageUrl?.absoluteString, forKey: .discoverImageUrl) + try container.encode(hasZoomDiscoverImage, forKey: .hasZoomDiscoverImage) + } +} + + + + + public class ExploreService { - private let exploreURL = "https://services.kukaiwallet.workers.dev/v1/explore" + private let exploreURL = "https://services.kukai.app/v2/explore?encode=true" private let exploreCacheKey = "explore-cahce-key" private let networkService: NetworkService private let requestIfService: RequestIfService + private let networkType: TezosNodeClientConfig.NetworkType public var contractAddressToPrimaryKeyMap: [String: UUID] = [:] public var items: [UUID: ExploreItem] = [:] - public init(networkService: NetworkService) { + public init(networkService: NetworkService, networkType: TezosNodeClientConfig.NetworkType) { self.networkService = networkService self.requestIfService = RequestIfService(networkService: networkService) + self.networkType = networkType - let lastCache = self.requestIfService.lastCache(forKey: exploreCacheKey, responseType: [ExploreItem].self) - processRawData(items: lastCache ?? []) + let lastCache = self.requestIfService.lastCache(forKey: exploreCacheKey, responseType: ExploreResponse.self) + processRawData(item: lastCache, networkType: networkType) } @@ -137,14 +154,25 @@ public class ExploreService { } } - private func processRawData(items: [ExploreItem]) { - for (index, item) in items.enumerated() { + private func processRawData(item: ExploreResponse?, networkType: TezosNodeClientConfig.NetworkType) { + guard let exploreItem = item else { + return + } + + var exploreItemsToProcess: [ExploreItem] = [] + if networkType == .mainnet { + exploreItemsToProcess = exploreItem.environment.mainnet.contractAliases + } else { + exploreItemsToProcess = exploreItem.environment.ghostnet.contractAliases + } + + for (index, item) in exploreItemsToProcess.enumerated() { var temp = item temp.sortIndex = index self.items[item.primaryKey] = temp } - self.processQuickFindList(items: items) + self.processQuickFindList(items: exploreItemsToProcess) } @@ -158,13 +186,13 @@ public class ExploreService { } // Request from API, no more frequently than once per day, else read cache - self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: exploreCacheKey, responseType: [ExploreItem].self) { [weak self] result in + self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.second.rawValue, forKey: exploreCacheKey, responseType: ExploreResponse.self, isSecure: true) { [weak self] result in guard let response = try? result.get() else { completion(Result.failure(result.getFailure())) return } - self?.processRawData(items: response) + self?.processRawData(item: response, networkType: self?.networkType ?? .mainnet) completion(Result.success(true)) } diff --git a/Kukai Mobile/Services/RequestIfService.swift b/Kukai Mobile/Services/RequestIfService.swift index 0f865831..446dc156 100644 --- a/Kukai Mobile/Services/RequestIfService.swift +++ b/Kukai Mobile/Services/RequestIfService.swift @@ -7,6 +7,13 @@ import Foundation import KukaiCoreSwift +import KukaiCryptoSwift +import OSLog + +private struct SecureServiceObject: Codable { + let data: String + let signature: String +} public class RequestIfService { @@ -36,13 +43,32 @@ public class RequestIfService { Send a request to a URL, only if a given time has passed since last request, else return cached version. Useful for situations where you don't want to overload a server, enforcing that its only called once per day (for example), while avoiding constantly wrapping functions in date checks */ - public func request(url: URL, withBody body: Data?, ifElapsedGreaterThan: TimeInterval, forKey key: String, responseType: T.Type, completion: @escaping ((Result) -> Void)) { - - let currentTimestmap = Date().timeIntervalSince1970 - let lastObj = DiskService.read(type: StorageObject.self, fromFileName: key) + public func request(url: URL, withBody body: Data?, ifElapsedGreaterThan: TimeInterval, forKey key: String, responseType: T.Type, isSecure: Bool = false, completion: @escaping ((Result) -> Void)) { - if lastObj == nil || (currentTimestmap - (lastObj?.lastRequested ?? currentTimestmap)) > ifElapsedGreaterThan { + if let validObject = checkIfCached(forKey: key, ifElapsedGreaterThan: ifElapsedGreaterThan, forType: T.self) { + + // If we have an object sotred on disk, that was stored within the timeframe, extract and return it + completion(Result.success(validObject)) + + } else if isSecure { + + // Else if its a secure endpoint, fetch, validate, parse and store the data + self.networkService.request(url: url, isPOST: body != nil, withBody: body, forReturnType: SecureServiceObject.self) { [weak self] result in + guard let res = try? result.get() else { + completion(Result.failure(result.getFailure())) + return + } + + if let validObject = self?.validate(secureObject: res, responseType: T.self) { + let _ = DiskService.write(encodable: StorageObject(lastRequested: Date().timeIntervalSince1970, storedData: validObject), toFileName: key) + completion(Result.success(validObject)) + } else { + completion(Result.failure(KukaiError.unknown(withString: "Unable to parse secure object"))) + } + } + } else { + // Else if not secure endpoint, just fetch and store self.networkService.request(url: url, isPOST: body != nil, withBody: body, forReturnType: T.self) { result in guard let res = try? result.get() else { completion(Result.failure(result.getFailure())) @@ -52,13 +78,21 @@ public class RequestIfService { let _ = DiskService.write(encodable: StorageObject(lastRequested: Date().timeIntervalSince1970, storedData: res), toFileName: key) completion(Result.success(res)) } + } + } + + private func checkIfCached(forKey key: String, ifElapsedGreaterThan: TimeInterval, forType: T.Type) -> T? { + let currentTimestmap = Date().timeIntervalSince1970 + let lastObj = DiskService.read(type: StorageObject.self, fromFileName: key) + + if lastObj == nil || (currentTimestmap - (lastObj?.lastRequested ?? currentTimestmap)) > ifElapsedGreaterThan { + return nil } else if let obj = lastObj { - completion(Result.success(obj.storedData)) - - } else { - completion(Result.failure(KukaiError.unknown())) + return obj.storedData } + + return nil } public func lastCache(forKey key: String, responseType: T.Type) -> T? { @@ -69,4 +103,23 @@ public class RequestIfService { public func delete(key: String) -> Bool { return DiskService.delete(fileName: key) } + + private func validate(secureObject: SecureServiceObject, responseType: T.Type) -> T? { + guard let publicKeyData = try? Data(hexString: "d71729958d14ba994b9bf29816f9710bd944d0ed7dc3e5a58a31532ca87e06f6"), + let signatureData = try? Data(hexString: secureObject.signature), + let data = Data(base64Encoded: secureObject.data) + else { + Logger.app.error("RequestIfService unable to setup secure data processing") + return nil + } + + let publicKey = PublicKey(publicKeyData.bytes, signingCurve: .ed25519) + let valid = publicKey.verify(message: data.bytes, signature: signatureData.bytes) + + if valid { + return try? JSONDecoder().decode(T.self, from: data) + } + + return nil + } }