@onekeyfe/react-native-bundle-update 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/ReactNativeBundleUpdate.podspec +34 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +139 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +1409 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt +24 -0
  11. package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
  12. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
  13. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  14. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  15. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  16. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  17. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  18. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  19. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  20. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  21. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
  22. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
  23. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
  24. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
  25. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
  26. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
  27. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
  28. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
  29. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
  30. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
  31. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
  32. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
  33. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
  34. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
  35. package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
  36. package/ios/ReactNativeBundleUpdate.swift +1338 -0
  37. package/lib/module/ReactNativeBundleUpdate.nitro.js +4 -0
  38. package/lib/module/ReactNativeBundleUpdate.nitro.js.map +1 -0
  39. package/lib/module/index.js +6 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +101 -0
  44. package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +4 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/nitro.json +17 -0
  48. package/nitrogen/generated/android/c++/JAscFileInfo.hpp +65 -0
  49. package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +77 -0
  50. package/nitrogen/generated/android/c++/JBundleDownloadEvent.hpp +65 -0
  51. package/nitrogen/generated/android/c++/JBundleDownloadParams.hpp +73 -0
  52. package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +73 -0
  53. package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +69 -0
  54. package/nitrogen/generated/android/c++/JBundleSwitchParams.hpp +65 -0
  55. package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +73 -0
  56. package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +69 -0
  57. package/nitrogen/generated/android/c++/JFallbackBundleInfo.hpp +65 -0
  58. package/nitrogen/generated/android/c++/JFunc_void_BundleDownloadEvent.hpp +78 -0
  59. package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.cpp +486 -0
  60. package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.hpp +89 -0
  61. package/nitrogen/generated/android/c++/JLocalBundleInfo.hpp +61 -0
  62. package/nitrogen/generated/android/c++/JTestResult.hpp +61 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/AscFileInfo.kt +44 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +53 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadEvent.kt +44 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadParams.kt +50 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +50 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +47 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleSwitchParams.kt +44 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +50 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +47 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/FallbackBundleInfo.kt +44 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/Func_void_BundleDownloadEvent.kt +80 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/HybridReactNativeBundleUpdateSpec.kt +159 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/LocalBundleInfo.kt +41 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/TestResult.kt +41 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/reactnativebundleupdateOnLoad.kt +35 -0
  78. package/nitrogen/generated/android/reactnativebundleupdate+autolinking.cmake +81 -0
  79. package/nitrogen/generated/android/reactnativebundleupdate+autolinking.gradle +27 -0
  80. package/nitrogen/generated/android/reactnativebundleupdateOnLoad.cpp +46 -0
  81. package/nitrogen/generated/android/reactnativebundleupdateOnLoad.hpp +25 -0
  82. package/nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb +60 -0
  83. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.cpp +113 -0
  84. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.hpp +513 -0
  85. package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Umbrella.hpp +83 -0
  86. package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.mm +33 -0
  87. package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.swift +25 -0
  88. package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.cpp +11 -0
  89. package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.hpp +304 -0
  90. package/nitrogen/generated/ios/swift/AscFileInfo.swift +58 -0
  91. package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +91 -0
  92. package/nitrogen/generated/ios/swift/BundleDownloadEvent.swift +58 -0
  93. package/nitrogen/generated/ios/swift/BundleDownloadParams.swift +80 -0
  94. package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +80 -0
  95. package/nitrogen/generated/ios/swift/BundleInstallParams.swift +69 -0
  96. package/nitrogen/generated/ios/swift/BundleSwitchParams.swift +58 -0
  97. package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +80 -0
  98. package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +69 -0
  99. package/nitrogen/generated/ios/swift/FallbackBundleInfo.swift +58 -0
  100. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  101. package/nitrogen/generated/ios/swift/Func_void_BundleDownloadEvent.swift +47 -0
  102. package/nitrogen/generated/ios/swift/Func_void_BundleDownloadResult.swift +47 -0
  103. package/nitrogen/generated/ios/swift/Func_void_TestResult.swift +47 -0
  104. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  105. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  106. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  107. package/nitrogen/generated/ios/swift/Func_void_std__vector_AscFileInfo_.swift +47 -0
  108. package/nitrogen/generated/ios/swift/Func_void_std__vector_FallbackBundleInfo_.swift +47 -0
  109. package/nitrogen/generated/ios/swift/Func_void_std__vector_LocalBundleInfo_.swift +47 -0
  110. package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec.swift +80 -0
  111. package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec_cxx.swift +595 -0
  112. package/nitrogen/generated/ios/swift/LocalBundleInfo.swift +47 -0
  113. package/nitrogen/generated/ios/swift/TestResult.swift +47 -0
  114. package/nitrogen/generated/shared/c++/AscFileInfo.hpp +83 -0
  115. package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +95 -0
  116. package/nitrogen/generated/shared/c++/BundleDownloadEvent.hpp +83 -0
  117. package/nitrogen/generated/shared/c++/BundleDownloadParams.hpp +91 -0
  118. package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +91 -0
  119. package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +87 -0
  120. package/nitrogen/generated/shared/c++/BundleSwitchParams.hpp +83 -0
  121. package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +91 -0
  122. package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +87 -0
  123. package/nitrogen/generated/shared/c++/FallbackBundleInfo.hpp +83 -0
  124. package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.cpp +45 -0
  125. package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.hpp +124 -0
  126. package/nitrogen/generated/shared/c++/LocalBundleInfo.hpp +79 -0
  127. package/nitrogen/generated/shared/c++/TestResult.hpp +79 -0
  128. package/package.json +169 -0
  129. package/src/ReactNativeBundleUpdate.nitro.ts +143 -0
  130. package/src/index.tsx +8 -0
@@ -0,0 +1,1338 @@
1
+ import NitroModules
2
+ import ReactNativeNativeLogger
3
+ import Foundation
4
+ import CommonCrypto
5
+ import Gopenpgp
6
+ import SSZipArchive
7
+ import MMKV
8
+
9
+ // OneKey GPG public key for signature verification
10
+ private let GPG_PUBLIC_KEY = """
11
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
12
+
13
+ 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
60
+ P2Q5dClenjjjVA==
61
+ =F0np
62
+ -----END PGP PUBLIC KEY BLOCK-----
63
+ """
64
+
65
+ // Public static store for AppDelegate access (called before JS starts)
66
+ public class BundleUpdateStore {
67
+ private static let bundlePrefsKey = "currentBundleVersion"
68
+ private static let nativeVersionKey = "nativeVersion"
69
+
70
+ public static func documentDirectory() -> String {
71
+ NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
72
+ }
73
+
74
+ public static func downloadBundleDir() -> String {
75
+ let dir = (documentDirectory() as NSString).appendingPathComponent("onekey-bundle-download")
76
+ let fm = FileManager.default
77
+ if !fm.fileExists(atPath: dir) {
78
+ try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true)
79
+ }
80
+ return dir
81
+ }
82
+
83
+ public static func bundleDir() -> String {
84
+ let dir = (documentDirectory() as NSString).appendingPathComponent("onekey-bundle")
85
+ let fm = FileManager.default
86
+ if !fm.fileExists(atPath: dir) {
87
+ try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true)
88
+ }
89
+ return dir
90
+ }
91
+
92
+ public static func ascDir() -> String {
93
+ let dir = (bundleDir() as NSString).appendingPathComponent("asc")
94
+ let fm = FileManager.default
95
+ if !fm.fileExists(atPath: dir) {
96
+ try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true)
97
+ }
98
+ return dir
99
+ }
100
+
101
+ public static func signatureFilePath(_ version: String) -> String {
102
+ return (ascDir() as NSString).appendingPathComponent("\(version)-signature.asc")
103
+ }
104
+
105
+ public static func writeSignatureFile(_ version: String, signature: String) {
106
+ let path = signatureFilePath(version)
107
+ let existed = FileManager.default.fileExists(atPath: path)
108
+ try? signature.write(toFile: path, atomically: true, encoding: .utf8)
109
+ let fileSize = (try? FileManager.default.attributesOfItem(atPath: path)[.size] as? UInt64) ?? 0
110
+ OneKeyLog.info("BundleUpdate", "writeSignatureFile: version=\(version), existed=\(existed), size=\(fileSize), path=\(path)")
111
+ }
112
+
113
+ public static func readSignatureFile(_ version: String) -> String {
114
+ let path = signatureFilePath(version)
115
+ guard FileManager.default.fileExists(atPath: path) else {
116
+ OneKeyLog.debug("BundleUpdate", "readSignatureFile: not found for version=\(version)")
117
+ return ""
118
+ }
119
+ let content = (try? String(contentsOfFile: path, encoding: .utf8)) ?? ""
120
+ OneKeyLog.debug("BundleUpdate", "readSignatureFile: version=\(version), size=\(content.count)")
121
+ return content
122
+ }
123
+
124
+ public static func deleteSignatureFile(_ version: String) {
125
+ let path = signatureFilePath(version)
126
+ try? FileManager.default.removeItem(atPath: path)
127
+ }
128
+
129
+ public static func getCurrentNativeVersion() -> String {
130
+ Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
131
+ }
132
+
133
+ public static func currentBundleVersion() -> String? {
134
+ UserDefaults.standard.string(forKey: bundlePrefsKey)
135
+ }
136
+
137
+ public static func setCurrentBundleVersion(_ version: String) {
138
+ UserDefaults.standard.set(version, forKey: bundlePrefsKey)
139
+ UserDefaults.standard.synchronize()
140
+ }
141
+
142
+ public static func currentBundleDir() -> String? {
143
+ guard let folderName = currentBundleVersion() else { return nil }
144
+ return (bundleDir() as NSString).appendingPathComponent(folderName)
145
+ }
146
+
147
+ public static func getWebEmbedPath() -> String {
148
+ guard let dir = currentBundleDir() else { return "" }
149
+ return (dir as NSString).appendingPathComponent("web-embed")
150
+ }
151
+
152
+ public static func calculateSHA256(_ filePath: String) -> String? {
153
+ guard let fileHandle = FileHandle(forReadingAtPath: filePath) else { return nil }
154
+ defer { fileHandle.closeFile() }
155
+
156
+ var context = CC_SHA256_CTX()
157
+ CC_SHA256_Init(&context)
158
+ while autoreleasepool(invoking: {
159
+ let data = fileHandle.readData(ofLength: 8192)
160
+ if data.count > 0 {
161
+ data.withUnsafeBytes { CC_SHA256_Update(&context, $0.baseAddress, CC_LONG(data.count)) }
162
+ return true
163
+ }
164
+ return false
165
+ }) {}
166
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
167
+ CC_SHA256_Final(&hash, &context)
168
+ return hash.map { String(format: "%02x", $0) }.joined()
169
+ }
170
+
171
+ public static func getNativeVersion() -> String? {
172
+ UserDefaults.standard.string(forKey: nativeVersionKey)
173
+ }
174
+
175
+ public static func setNativeVersion(_ version: String) {
176
+ UserDefaults.standard.set(version, forKey: nativeVersionKey)
177
+ UserDefaults.standard.synchronize()
178
+ }
179
+
180
+ public static func getMetadataFilePath(_ currentBundleVersion: String) -> String? {
181
+ let path = (bundleDir() as NSString)
182
+ .appendingPathComponent(currentBundleVersion)
183
+ let metadataPath = (path as NSString).appendingPathComponent("metadata.json")
184
+ guard FileManager.default.fileExists(atPath: metadataPath) else { return nil }
185
+ return metadataPath
186
+ }
187
+
188
+ public static func getMetadataFileContent(_ currentBundleVersion: String) -> [String: String]? {
189
+ guard let path = getMetadataFilePath(currentBundleVersion),
190
+ let data = FileManager.default.contents(atPath: path),
191
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { return nil }
192
+ return json
193
+ }
194
+
195
+ /// Returns true if OneKey developer mode (DevSettings) is enabled.
196
+ /// Reads the persisted value from MMKV storage written by the JS ServiceDevSetting layer.
197
+ public static func isDevSettingsEnabled() -> Bool {
198
+ // Ensure MMKV is initialized (safe to call multiple times)
199
+ MMKV.initialize(rootDir: nil)
200
+ guard let mmkv = MMKV(mmapID: "onekey-app-setting") else { return false }
201
+ return mmkv.bool(forKey: "onekey_developer_mode_enabled", defaultValue: false)
202
+ }
203
+
204
+ /// Returns true if the skip-GPG-verification toggle is enabled in developer settings.
205
+ /// Reads the persisted value from MMKV storage (key: onekey_bundle_skip_gpg_verification,
206
+ /// instance: onekey-app-setting).
207
+ public static func isSkipGPGEnabled() -> Bool {
208
+ MMKV.initialize(rootDir: nil)
209
+ guard let mmkv = MMKV(mmapID: "onekey-app-setting") else { return false }
210
+ return mmkv.bool(forKey: "onekey_bundle_skip_gpg_verification", defaultValue: false)
211
+ }
212
+
213
+ public static func readMetadataFileSha256(_ signature: String) -> String? {
214
+ guard !signature.isEmpty else { return nil }
215
+
216
+ // GPG cleartext signature verification is required
217
+ guard let sha256 = verifyGPGAndExtractSha256(signature) else {
218
+ OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed, rejecting unsigned content")
219
+ return nil
220
+ }
221
+ return sha256
222
+ }
223
+
224
+ /// Verify a PGP cleartext-signed message and extract the sha256 from the signed JSON body.
225
+ /// Uses Gopenpgp framework (vendored xcframework).
226
+ /// Returns nil if verification fails.
227
+ public static func verifyGPGAndExtractSha256(_ signature: String) -> String? {
228
+ // Check if this looks like a PGP signed message
229
+ guard signature.contains("-----BEGIN PGP SIGNED MESSAGE-----") else {
230
+ return nil
231
+ }
232
+
233
+ // 1. Load public key
234
+ guard let pubKey = CryptoKey(fromArmored: GPG_PUBLIC_KEY) else {
235
+ OneKeyLog.error("BundleUpdate", "Failed to parse GPG public key")
236
+ return nil
237
+ }
238
+
239
+ // 2. Get PGP handle
240
+ guard let pgp = CryptoPGP() else {
241
+ OneKeyLog.error("BundleUpdate", "Failed to create PGPHandle")
242
+ return nil
243
+ }
244
+
245
+ // 3. Build verify handle: pgp.verify().verificationKey(pubKey).new()
246
+ guard let verifyBuilder = pgp.verify() else {
247
+ OneKeyLog.error("BundleUpdate", "Failed to get verify builder")
248
+ return nil
249
+ }
250
+ guard let builderWithKey = verifyBuilder.verificationKey(pubKey) else {
251
+ OneKeyLog.error("BundleUpdate", "Failed to set verification key")
252
+ return nil
253
+ }
254
+
255
+ let verifyHandle: any CryptoPGPVerifyProtocol
256
+ do {
257
+ verifyHandle = try builderWithKey.new()
258
+ } catch {
259
+ OneKeyLog.error("BundleUpdate", "Failed to create verify handle: \(error.localizedDescription)")
260
+ return nil
261
+ }
262
+
263
+ // 4. Verify cleartext
264
+ guard let signatureData = signature.data(using: .utf8) else {
265
+ return nil
266
+ }
267
+
268
+ let cleartextResult: CryptoVerifyCleartextResult
269
+ do {
270
+ cleartextResult = try verifyHandle.verifyCleartext(signatureData)
271
+ } catch {
272
+ OneKeyLog.error("BundleUpdate", "GPG verification error: \(error.localizedDescription)")
273
+ return nil
274
+ }
275
+
276
+ // 5. Check signature error
277
+ do {
278
+ try cleartextResult.signatureError()
279
+ } catch {
280
+ OneKeyLog.error("BundleUpdate", "GPG signature invalid: \(error.localizedDescription)")
281
+ return nil
282
+ }
283
+
284
+ // 6. Get cleartext
285
+ guard let cleartextData = cleartextResult.cleartext() else {
286
+ OneKeyLog.error("BundleUpdate", "Failed to extract cleartext from GPG result")
287
+ return nil
288
+ }
289
+
290
+ guard let text = String(data: cleartextData, encoding: .utf8) else {
291
+ return nil
292
+ }
293
+
294
+ // 7. Parse JSON and extract sha256
295
+ guard let jsonData = text.data(using: .utf8),
296
+ let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
297
+ let sha256 = json["sha256"] as? String else {
298
+ OneKeyLog.error("BundleUpdate", "Failed to parse cleartext JSON")
299
+ return nil
300
+ }
301
+
302
+ OneKeyLog.info("BundleUpdate", "GPG verification succeeded, sha256: \(sha256)")
303
+ return sha256
304
+ }
305
+
306
+ public static func validateMetadataFileSha256(_ currentBundleVersion: String, signature: String) -> Bool {
307
+ guard let metadataFilePath = getMetadataFilePath(currentBundleVersion) else {
308
+ OneKeyLog.debug("BundleUpdate", "metadataFilePath is null")
309
+ return false
310
+ }
311
+ guard let extractedSha256 = readMetadataFileSha256(signature), !extractedSha256.isEmpty else {
312
+ return false
313
+ }
314
+ guard let calculatedSha256 = calculateSHA256(metadataFilePath) else { return false }
315
+ return calculatedSha256.secureCompare(extractedSha256)
316
+ }
317
+
318
+ public static func validateExtractedPathSafety(_ destination: String) -> Bool {
319
+ let fm = FileManager.default
320
+ let resolvedDestination = (destination as NSString).resolvingSymlinksInPath
321
+
322
+ guard let enumerator = fm.enumerator(atPath: destination) else { return true }
323
+ while let file = enumerator.nextObject() as? String {
324
+ let fullPath = (destination as NSString).appendingPathComponent(file)
325
+ if let attrs = enumerator.fileAttributes,
326
+ attrs[.type] as? FileAttributeType == .typeSymbolicLink {
327
+ OneKeyLog.error("BundleUpdate", "Symlink detected in extracted bundle: \(file)")
328
+ return false
329
+ }
330
+ let resolvedPath = (fullPath as NSString).resolvingSymlinksInPath
331
+ if !resolvedPath.hasPrefix(resolvedDestination) {
332
+ OneKeyLog.error("BundleUpdate", "Path traversal detected in extracted bundle: \(file)")
333
+ return false
334
+ }
335
+ }
336
+ return true
337
+ }
338
+
339
+ public static func validateAllFilesInDir(_ dirPath: String, metadata: [String: String], appVersion: String, bundleVersion: String) -> Bool {
340
+ let parentBundleDir = bundleDir()
341
+ let folderName = "\(appVersion)-\(bundleVersion)"
342
+ let jsBundleDir = (parentBundleDir as NSString).appendingPathComponent(folderName) + "/"
343
+ let fm = FileManager.default
344
+
345
+ guard let enumerator = fm.enumerator(atPath: dirPath) else { return false }
346
+ while let file = enumerator.nextObject() as? String {
347
+ if file.contains("metadata.json") || file.contains(".DS_Store") { continue }
348
+ let fullPath = (dirPath as NSString).appendingPathComponent(file)
349
+ var isDir: ObjCBool = false
350
+ if fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue { continue }
351
+
352
+ let relativePath = fullPath.replacingOccurrences(of: jsBundleDir, with: "")
353
+ guard let expectedSHA256 = metadata[relativePath] else {
354
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] File on disk not found in metadata: \(relativePath)")
355
+ return false
356
+ }
357
+ guard let actualSHA256 = calculateSHA256(fullPath) else {
358
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] Failed to calculate SHA256 for file: \(relativePath)")
359
+ return false
360
+ }
361
+ if !expectedSHA256.secureCompare(actualSHA256) {
362
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] SHA256 mismatch for \(relativePath)")
363
+ return false
364
+ }
365
+ }
366
+
367
+ // Verify completeness
368
+ for key in metadata.keys {
369
+ let expectedFilePath = jsBundleDir + key
370
+ if !fm.fileExists(atPath: expectedFilePath) {
371
+ OneKeyLog.error("BundleUpdate", "[bundle-verify] File listed in metadata but missing on disk: \(key)")
372
+ return false
373
+ }
374
+ }
375
+ return true
376
+ }
377
+
378
+ public static func currentBundleMainJSBundle() -> String? {
379
+ guard let currentBundleVer = currentBundleVersion() else {
380
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored")
381
+ return nil
382
+ }
383
+
384
+ let currentAppVersion = getCurrentNativeVersion()
385
+ guard let prevNativeVersion = getNativeVersion() else {
386
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: prevNativeVersion is nil")
387
+ return nil
388
+ }
389
+
390
+ OneKeyLog.info("BundleUpdate", "currentAppVersion: \(currentAppVersion), currentBundleVersion: \(currentBundleVer), prevNativeVersion: \(prevNativeVersion)")
391
+
392
+ if currentAppVersion != prevNativeVersion {
393
+ OneKeyLog.info("BundleUpdate", "currentAppVersion is not equal to prevNativeVersion \(currentAppVersion) \(prevNativeVersion)")
394
+ let ud = UserDefaults.standard
395
+ if let cbv = ud.string(forKey: "currentBundleVersion") {
396
+ deleteSignatureFile(cbv)
397
+ ud.removeObject(forKey: "currentBundleVersion")
398
+ }
399
+ ud.synchronize()
400
+ return nil
401
+ }
402
+
403
+ guard let folderName = currentBundleDir(),
404
+ FileManager.default.fileExists(atPath: folderName) else {
405
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: currentBundleDir does not exist")
406
+ return nil
407
+ }
408
+
409
+ let signature = readSignatureFile(currentBundleVer)
410
+ OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=\(signature.count)")
411
+
412
+ let devSettingsEnabled = isDevSettingsEnabled()
413
+ if devSettingsEnabled {
414
+ OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)")
415
+ }
416
+ if !devSettingsEnabled && !validateMetadataFileSha256(currentBundleVer, signature: signature) {
417
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=\(signature.count)")
418
+ return nil
419
+ }
420
+
421
+ guard let metadata = getMetadataFileContent(currentBundleVer) else {
422
+ OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getMetadataFileContent returned nil")
423
+ return nil
424
+ }
425
+
426
+ if let dashRange = currentBundleVer.range(of: "-", options: .backwards) {
427
+ let appVer = String(currentBundleVer[currentBundleVer.startIndex..<dashRange.lowerBound])
428
+ let bundleVer = String(currentBundleVer[dashRange.upperBound...])
429
+ if !validateAllFilesInDir(folderName, metadata: metadata, appVersion: appVer, bundleVersion: bundleVer) {
430
+ OneKeyLog.info("BundleUpdate", "validateAllFilesInDir failed on startup")
431
+ return nil
432
+ }
433
+ }
434
+
435
+ let mainJSBundle = (folderName as NSString).appendingPathComponent("main.jsbundle.hbc")
436
+ guard FileManager.default.fileExists(atPath: mainJSBundle) else {
437
+ OneKeyLog.info("BundleUpdate", "mainJSBundleFile does not exist")
438
+ return nil
439
+ }
440
+ return mainJSBundle
441
+ }
442
+
443
+ // Fallback data management
444
+ static func getFallbackUpdateBundleDataPath() -> String {
445
+ let path = (bundleDir() as NSString).appendingPathComponent("fallbackUpdateBundleData.json")
446
+ if !FileManager.default.fileExists(atPath: path) {
447
+ FileManager.default.createFile(atPath: path, contents: nil)
448
+ }
449
+ return path
450
+ }
451
+
452
+ static func readFallbackUpdateBundleDataFile() -> [[String: String]] {
453
+ let path = getFallbackUpdateBundleDataPath()
454
+ guard let content = try? String(contentsOfFile: path, encoding: .utf8),
455
+ !content.isEmpty,
456
+ let data = content.data(using: .utf8),
457
+ let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: String]] else {
458
+ return []
459
+ }
460
+ return arr
461
+ }
462
+
463
+ static func writeFallbackUpdateBundleDataFile(_ data: [[String: String]]) {
464
+ let path = getFallbackUpdateBundleDataPath()
465
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: data),
466
+ let jsonString = String(data: jsonData, encoding: .utf8) else { return }
467
+ try? jsonString.write(toFile: path, atomically: true, encoding: .utf8)
468
+ }
469
+
470
+ public static func clearUpdateBundleData() {
471
+ let bDir = bundleDir()
472
+ let fm = FileManager.default
473
+ if fm.fileExists(atPath: bDir) {
474
+ // This also deletes asc/ directory containing all signature files
475
+ try? fm.removeItem(atPath: bDir)
476
+ }
477
+ let ud = UserDefaults.standard
478
+ // Legacy cleanup: remove signature from UserDefaults if present
479
+ if let cbv = currentBundleVersion() {
480
+ ud.removeObject(forKey: cbv)
481
+ }
482
+ ud.removeObject(forKey: bundlePrefsKey)
483
+ ud.synchronize()
484
+ }
485
+ }
486
+
487
+ // Constant-time string comparison to prevent timing attacks on hash comparisons
488
+ private extension String {
489
+ func secureCompare(_ other: String) -> Bool {
490
+ let lhs = Array(self.utf8)
491
+ let rhs = Array(other.utf8)
492
+ guard lhs.count == rhs.count else { return false }
493
+ var result: UInt8 = 0
494
+ for i in 0..<lhs.count {
495
+ result |= lhs[i] ^ rhs[i]
496
+ }
497
+ return result == 0
498
+ }
499
+ }
500
+
501
+ // Version string sanitization
502
+ private extension String {
503
+ var isSafeVersionString: Bool {
504
+ let allowedChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: ".-_"))
505
+ return !self.isEmpty && self.unicodeScalars.allSatisfy { allowedChars.contains($0) }
506
+ }
507
+ }
508
+
509
+ /// URLSession delegate that handles download progress and HTTPS redirect validation
510
+ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
511
+ /// Called with progress percentage (0-100) during download
512
+ var onProgress: ((Int) -> Void)?
513
+
514
+ /// Continuation to bridge delegate callbacks → async/await
515
+ private var continuation: CheckedContinuation<(URL, URLResponse), Error>?
516
+ private var tempFileURL: URL?
517
+ private let lock = NSLock()
518
+
519
+ func setContinuation(_ cont: CheckedContinuation<(URL, URLResponse), Error>) {
520
+ lock.lock()
521
+ continuation = cont
522
+ lock.unlock()
523
+ }
524
+
525
+ // MARK: - URLSessionDownloadDelegate
526
+
527
+ private var prevProgress: Int = -1
528
+
529
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
530
+ guard totalBytesExpectedToWrite > 0 else { return }
531
+ let progress = Int((totalBytesWritten * 100) / totalBytesExpectedToWrite)
532
+ if progress != prevProgress {
533
+ OneKeyLog.info("BundleUpdate", "download progress: \(progress)% (\(totalBytesWritten)/\(totalBytesExpectedToWrite))")
534
+ prevProgress = progress
535
+ onProgress?(progress)
536
+ }
537
+ }
538
+
539
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
540
+ // Copy to a temp location because the file at `location` is deleted after this method returns
541
+ let tempPath = NSTemporaryDirectory() + UUID().uuidString
542
+ let dest = URL(fileURLWithPath: tempPath)
543
+ do {
544
+ try FileManager.default.copyItem(at: location, to: dest)
545
+ tempFileURL = dest
546
+ } catch {
547
+ OneKeyLog.error("BundleUpdate", "Failed to copy downloaded file: \(error)")
548
+ }
549
+ }
550
+
551
+ // MARK: - URLSessionTaskDelegate
552
+
553
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
554
+ lock.lock()
555
+ let cont = continuation
556
+ continuation = nil
557
+ lock.unlock()
558
+
559
+ if let error = error {
560
+ cont?.resume(throwing: error)
561
+ } else if let tempURL = tempFileURL, let response = task.response {
562
+ cont?.resume(returning: (tempURL, response))
563
+ } else {
564
+ cont?.resume(throwing: NSError(domain: "BundleUpdate", code: -1,
565
+ userInfo: [NSLocalizedDescriptionKey: "Download completed without file"]))
566
+ }
567
+ }
568
+
569
+ func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
570
+ if request.url?.scheme?.lowercased() != "https" {
571
+ OneKeyLog.error("BundleUpdate", "Blocked redirect to non-HTTPS URL")
572
+ completionHandler(nil)
573
+ } else {
574
+ completionHandler(request)
575
+ }
576
+ }
577
+
578
+ /// Reset state for reuse
579
+ func reset() {
580
+ lock.lock()
581
+ continuation = nil
582
+ lock.unlock()
583
+ tempFileURL = nil
584
+ onProgress = nil
585
+ prevProgress = -1
586
+ }
587
+ }
588
+
589
+ // Listener support
590
+ private struct BundleListener {
591
+ let id: Double
592
+ let callback: (BundleDownloadEvent) -> Void
593
+ }
594
+
595
+ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
596
+
597
+ // Serial queue protects mutable state (listeners, nextListenerId, isDownloading)
598
+ private let stateQueue = DispatchQueue(label: "so.onekey.bundleupdate.state")
599
+ private var listeners: [BundleListener] = []
600
+ private var nextListenerId: Double = 1
601
+ private var isDownloading = false
602
+ private var urlSession: URLSession?
603
+ private var downloadDelegate: DownloadDelegate?
604
+ private var downloadFilePath: String?
605
+ private var downloadSha256: String?
606
+
607
+ private func createURLSession() -> URLSession {
608
+ let config = URLSessionConfiguration.default
609
+ config.tlsMinimumSupportedProtocolVersion = .TLSv12
610
+ let delegate = DownloadDelegate()
611
+ self.downloadDelegate = delegate
612
+ return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
613
+ }
614
+
615
+ override init() {
616
+ super.init()
617
+ urlSession = createURLSession()
618
+ }
619
+
620
+ private func sendEvent(type: String, progress: Int = 0, message: String = "") {
621
+ let event = BundleDownloadEvent(type: type, progress: Double(progress), message: message)
622
+ let currentListeners = stateQueue.sync { self.listeners }
623
+ for listener in currentListeners {
624
+ do {
625
+ listener.callback(event)
626
+ } catch {
627
+ OneKeyLog.error("BundleUpdate", "Error sending event: \(error)")
628
+ }
629
+ }
630
+ }
631
+
632
+ func addDownloadListener(callback: @escaping (BundleDownloadEvent) -> Void) throws -> Double {
633
+ return stateQueue.sync {
634
+ let id = nextListenerId
635
+ nextListenerId += 1
636
+ listeners.append(BundleListener(id: id, callback: callback))
637
+ OneKeyLog.debug("BundleUpdate", "addDownloadListener: id=\(id), totalListeners=\(listeners.count)")
638
+ return id
639
+ }
640
+ }
641
+
642
+ func removeDownloadListener(id: Double) throws {
643
+ stateQueue.sync {
644
+ listeners.removeAll { $0.id == id }
645
+ OneKeyLog.debug("BundleUpdate", "removeDownloadListener: id=\(id), totalListeners=\(listeners.count)")
646
+ }
647
+ }
648
+
649
+ func downloadBundle(params: BundleDownloadParams) throws -> Promise<BundleDownloadResult> {
650
+ return Promise.async { [weak self] in
651
+ guard let self = self else { throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Module deallocated"]) }
652
+
653
+ let alreadyDownloading = self.stateQueue.sync { () -> Bool in
654
+ if self.isDownloading { return true }
655
+ self.isDownloading = true
656
+ return false
657
+ }
658
+ guard !alreadyDownloading else {
659
+ OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading")
660
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Already downloading"])
661
+ }
662
+ defer { self.stateQueue.sync { self.isDownloading = false } }
663
+
664
+ let appVersion = params.latestVersion
665
+ let bundleVersion = params.bundleVersion
666
+ let downloadUrl = params.downloadUrl
667
+ let sha256 = params.sha256
668
+
669
+ OneKeyLog.info("BundleUpdate", "downloadBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), fileSize=\(params.fileSize), url=\(downloadUrl)")
670
+
671
+ guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
672
+ OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
673
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string format"])
674
+ }
675
+
676
+ guard downloadUrl.hasPrefix("https://") else {
677
+ OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: \(downloadUrl)")
678
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle download URL must use HTTPS"])
679
+ }
680
+
681
+ let fileName = "\(appVersion)-\(bundleVersion).zip"
682
+ let filePath = (BundleUpdateStore.downloadBundleDir() as NSString).appendingPathComponent(fileName)
683
+
684
+ let result = BundleDownloadResult(
685
+ downloadedFile: filePath,
686
+ downloadUrl: downloadUrl,
687
+ latestVersion: appVersion,
688
+ bundleVersion: bundleVersion,
689
+ sha256: sha256
690
+ )
691
+
692
+ OneKeyLog.info("BundleUpdate", "downloadBundle: filePath=\(filePath)")
693
+
694
+ // Check if file already exists and is valid
695
+ if FileManager.default.fileExists(atPath: filePath) {
696
+ OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...")
697
+ if self.verifyBundleSHA256(filePath, sha256: sha256) {
698
+ OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
699
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
700
+ self?.sendEvent(type: "update/complete")
701
+ }
702
+ return result
703
+ } else {
704
+ OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
705
+ try? FileManager.default.removeItem(atPath: filePath)
706
+ }
707
+ }
708
+
709
+ // Download the file
710
+ guard let url = URL(string: downloadUrl) else {
711
+ OneKeyLog.error("BundleUpdate", "downloadBundle: invalid URL: \(downloadUrl)")
712
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
713
+ }
714
+
715
+ guard let session = self.urlSession else {
716
+ OneKeyLog.error("BundleUpdate", "downloadBundle: URLSession not initialized")
717
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "URLSession not initialized"])
718
+ }
719
+
720
+ self.sendEvent(type: "update/start")
721
+ OneKeyLog.info("BundleUpdate", "downloadBundle: starting download...")
722
+
723
+ let request = URLRequest(url: url)
724
+
725
+ // Use delegate-based download for real progress reporting
726
+ guard let delegate = self.downloadDelegate else {
727
+ OneKeyLog.error("BundleUpdate", "downloadBundle: download delegate not initialized")
728
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download delegate not initialized"])
729
+ }
730
+ delegate.reset()
731
+ delegate.onProgress = { [weak self] progress in
732
+ self?.sendEvent(type: "update/downloading", progress: progress)
733
+ }
734
+
735
+ let (tempURL, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(URL, URLResponse), Error>) in
736
+ delegate.setContinuation(continuation)
737
+ let task = session.downloadTask(with: request)
738
+ task.resume()
739
+ }
740
+
741
+ // Verify HTTPS was maintained (no HTTP redirect)
742
+ if let httpResponse = response as? HTTPURLResponse,
743
+ let responseUrl = httpResponse.url,
744
+ responseUrl.scheme?.lowercased() != "https" {
745
+ OneKeyLog.error("BundleUpdate", "downloadBundle: redirected to non-HTTPS URL: \(responseUrl)")
746
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download was redirected to non-HTTPS URL"])
747
+ }
748
+
749
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
750
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
751
+ OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=\(statusCode)")
752
+ self.sendEvent(type: "update/error", message: "HTTP error \(statusCode)")
753
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download failed with HTTP \(statusCode)"])
754
+ }
755
+
756
+ OneKeyLog.info("BundleUpdate", "downloadBundle: download finished, HTTP 200, moving to destination...")
757
+
758
+ // Move downloaded file to destination
759
+ let destDir = (filePath as NSString).deletingLastPathComponent
760
+ if !FileManager.default.fileExists(atPath: destDir) {
761
+ try FileManager.default.createDirectory(atPath: destDir, withIntermediateDirectories: true)
762
+ }
763
+ if FileManager.default.fileExists(atPath: filePath) {
764
+ try FileManager.default.removeItem(atPath: filePath)
765
+ }
766
+ try FileManager.default.moveItem(at: tempURL, to: URL(fileURLWithPath: filePath))
767
+
768
+ // Verify SHA256
769
+ OneKeyLog.info("BundleUpdate", "downloadBundle: verifying SHA256...")
770
+ if !self.verifyBundleSHA256(filePath, sha256: sha256) {
771
+ try? FileManager.default.removeItem(atPath: filePath)
772
+ OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download")
773
+ self.sendEvent(type: "update/error", message: "Bundle signature verification failed")
774
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"])
775
+ }
776
+
777
+ self.sendEvent(type: "update/complete")
778
+ OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
779
+ return result
780
+ }
781
+ }
782
+
783
+ private func verifyBundleSHA256(_ bundlePath: String, sha256: String) -> Bool {
784
+ guard let calculated = BundleUpdateStore.calculateSHA256(bundlePath) else {
785
+ OneKeyLog.error("BundleUpdate", "verifyBundleSHA256: failed to calculate SHA256 for: \(bundlePath)")
786
+ return false
787
+ }
788
+ let isValid = calculated.secureCompare(sha256)
789
+ OneKeyLog.debug("BundleUpdate", "verifyBundleSHA256: path=\(bundlePath), expected=\(sha256.prefix(16))..., calculated=\(calculated.prefix(16))..., valid=\(isValid)")
790
+ return isValid
791
+ }
792
+
793
+ func downloadBundleASC(params: BundleDownloadASCParams) throws -> Promise<Void> {
794
+ return Promise.async {
795
+ let appVersion = params.latestVersion
796
+ let bundleVersion = params.bundleVersion
797
+ let signature = params.signature
798
+
799
+ OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)")
800
+
801
+ let storageKey = "\(appVersion)-\(bundleVersion)"
802
+ BundleUpdateStore.writeSignatureFile(storageKey, signature: signature)
803
+
804
+ OneKeyLog.info("BundleUpdate", "downloadBundleASC: stored signature for key=\(storageKey)")
805
+ }
806
+ }
807
+
808
+ func verifyBundleASC(params: BundleVerifyASCParams) throws -> Promise<Void> {
809
+ return Promise.async {
810
+ let filePath = params.downloadedFile
811
+ let sha256 = params.sha256
812
+ let appVersion = params.latestVersion
813
+ let bundleVersion = params.bundleVersion
814
+ let signature = params.signature
815
+
816
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath), signatureLength=\(signature.count)")
817
+
818
+ // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
819
+ let devSettings = BundleUpdateStore.isDevSettingsEnabled()
820
+ let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled()
821
+ let skipGPG = devSettings && skipGPGToggle
822
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle), skipGPG=\(skipGPG)")
823
+
824
+ if !skipGPG {
825
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...")
826
+ guard let calculated = BundleUpdateStore.calculateSHA256(filePath),
827
+ calculated.secureCompare(sha256) else {
828
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=\(filePath)")
829
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"])
830
+ }
831
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK")
832
+ } else {
833
+ OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)")
834
+ }
835
+
836
+ let folderName = "\(appVersion)-\(bundleVersion)"
837
+ let destination = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
838
+
839
+ // Check zip file size before extraction (decompression bomb protection)
840
+ let maxZipFileSize: UInt64 = 512 * 1024 * 1024 // 512 MB
841
+ if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath),
842
+ let fileSize = attrs[.size] as? UInt64 {
843
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: zip file size=\(fileSize) bytes")
844
+ if fileSize > maxZipFileSize {
845
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: zip file too large (\(fileSize) > \(maxZipFileSize))")
846
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Zip file exceeds maximum allowed size"])
847
+ }
848
+ }
849
+
850
+ // Unzip using SSZipArchive
851
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to \(destination)...")
852
+ do {
853
+ try SSZipArchive.unzipFile(atPath: filePath, toDestination: destination, overwrite: true, password: nil)
854
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed")
855
+ } catch {
856
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: unzip failed: \(error.localizedDescription)")
857
+ try? FileManager.default.removeItem(atPath: destination)
858
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to unzip bundle: \(error.localizedDescription)"])
859
+ }
860
+
861
+ // Validate extracted paths (symlinks, path traversal)
862
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating extracted path safety...")
863
+ if !BundleUpdateStore.validateExtractedPathSafety(destination) {
864
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: path traversal or symlink attack detected")
865
+ try? FileManager.default.removeItem(atPath: destination)
866
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Path traversal or symlink attack detected"])
867
+ }
868
+
869
+ let metadataJsonPath = (destination as NSString).appendingPathComponent("metadata.json")
870
+ guard FileManager.default.fileExists(atPath: metadataJsonPath) else {
871
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction")
872
+ try? FileManager.default.removeItem(atPath: destination)
873
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to read metadata.json"])
874
+ }
875
+
876
+ let currentBundleVersion = "\(appVersion)-\(bundleVersion)"
877
+ if !skipGPG {
878
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...")
879
+ if !BundleUpdateStore.validateMetadataFileSha256(currentBundleVersion, signature: signature) {
880
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed")
881
+ try? FileManager.default.removeItem(atPath: destination)
882
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"])
883
+ }
884
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK")
885
+ } else {
886
+ OneKeyLog.warn("BundleUpdate", "verifyBundleASC: GPG verification skipped (DevSettings enabled)")
887
+ }
888
+
889
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating all extracted files against metadata...")
890
+ guard let metadata = BundleUpdateStore.getMetadataFileContent(currentBundleVersion) else {
891
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: failed to read metadata.json content")
892
+ try? FileManager.default.removeItem(atPath: destination)
893
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to read metadata.json after extraction"])
894
+ }
895
+
896
+ if !BundleUpdateStore.validateAllFilesInDir(destination, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
897
+ OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed")
898
+ try? FileManager.default.removeItem(atPath: destination)
899
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Extracted files verification against metadata failed"])
900
+ }
901
+
902
+ OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
903
+ }
904
+ }
905
+
906
+ func verifyBundle(params: BundleVerifyParams) throws -> Promise<Void> {
907
+ return Promise.async {
908
+ let filePath = params.downloadedFile
909
+ let sha256 = params.sha256
910
+ let appVersion = params.latestVersion
911
+ let bundleVersion = params.bundleVersion
912
+
913
+ OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath)")
914
+
915
+ // Verify SHA256 of the downloaded file
916
+ guard let calculated = BundleUpdateStore.calculateSHA256(filePath) else {
917
+ OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=\(filePath)")
918
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to calculate SHA256"])
919
+ }
920
+ guard calculated.secureCompare(sha256) else {
921
+ OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=\(sha256.prefix(16))..., got=\(calculated.prefix(16))...")
922
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "SHA256 verification failed"])
923
+ }
924
+
925
+ OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
926
+ }
927
+ }
928
+
929
+ func installBundle(params: BundleInstallParams) throws -> Promise<Void> {
930
+ return Promise.async {
931
+ let appVersion = params.latestVersion
932
+ let bundleVersion = params.bundleVersion
933
+ let signature = params.signature
934
+
935
+ OneKeyLog.info("BundleUpdate", "installBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)")
936
+
937
+ // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
938
+ let devSettings = BundleUpdateStore.isDevSettingsEnabled()
939
+ let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled()
940
+ let skipGPG = devSettings && skipGPGToggle
941
+ OneKeyLog.info("BundleUpdate", "installBundle: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle), skipGPG=\(skipGPG)")
942
+
943
+ let folderName = "\(appVersion)-\(bundleVersion)"
944
+ let currentFolderName = BundleUpdateStore.currentBundleVersion()
945
+ OneKeyLog.info("BundleUpdate", "installBundle: target=\(folderName), current=\(currentFolderName ?? "nil")")
946
+
947
+ // Verify bundle directory exists
948
+ let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
949
+ guard FileManager.default.fileExists(atPath: bundleDirPath) else {
950
+ OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: \(bundleDirPath)")
951
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found: \(folderName)"])
952
+ }
953
+
954
+ let ud = UserDefaults.standard
955
+ ud.set(folderName, forKey: "currentBundleVersion")
956
+ if !signature.isEmpty {
957
+ BundleUpdateStore.writeSignatureFile(folderName, signature: signature)
958
+ }
959
+ let currentNativeVersion = BundleUpdateStore.getCurrentNativeVersion()
960
+ BundleUpdateStore.setNativeVersion(currentNativeVersion)
961
+ ud.synchronize()
962
+
963
+ // Manage fallback data
964
+ var fallbackData = BundleUpdateStore.readFallbackUpdateBundleDataFile()
965
+
966
+ if let current = currentFolderName,
967
+ let dashRange = current.range(of: "-", options: .backwards) {
968
+ let curAppVersion = String(current[current.startIndex..<dashRange.lowerBound])
969
+ let curBundleVersion = String(current[dashRange.upperBound...])
970
+ let curSignature = BundleUpdateStore.readSignatureFile(current)
971
+ if !curSignature.isEmpty {
972
+ fallbackData.append([
973
+ "appVersion": curAppVersion,
974
+ "bundleVersion": curBundleVersion,
975
+ "signature": curSignature
976
+ ])
977
+ }
978
+ }
979
+
980
+ // Keep max 3 fallback entries
981
+ if fallbackData.count > 3 {
982
+ let shifted = fallbackData.removeFirst()
983
+ if let shiftApp = shifted["appVersion"], let shiftBundle = shifted["bundleVersion"] {
984
+ let dirName = "\(shiftApp)-\(shiftBundle)"
985
+ BundleUpdateStore.deleteSignatureFile(dirName)
986
+ let oldPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(dirName)
987
+ if FileManager.default.fileExists(atPath: oldPath) {
988
+ try? FileManager.default.removeItem(atPath: oldPath)
989
+ }
990
+ }
991
+ }
992
+
993
+ BundleUpdateStore.writeFallbackUpdateBundleDataFile(fallbackData)
994
+ ud.synchronize()
995
+
996
+ OneKeyLog.info("BundleUpdate", "installBundle: completed successfully, installed version=\(folderName), fallbackCount=\(fallbackData.count)")
997
+ }
998
+ }
999
+
1000
+ func clearBundle() throws -> Promise<Void> {
1001
+ return Promise.async { [weak self] in
1002
+ OneKeyLog.info("BundleUpdate", "clearBundle: clearing download directory and cancelling downloads...")
1003
+ let downloadDir = BundleUpdateStore.downloadBundleDir()
1004
+ if FileManager.default.fileExists(atPath: downloadDir) {
1005
+ try FileManager.default.removeItem(atPath: downloadDir)
1006
+ OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted")
1007
+ } else {
1008
+ OneKeyLog.info("BundleUpdate", "clearBundle: download directory does not exist, skipping")
1009
+ }
1010
+ // Cancel all in-flight downloads by invalidating the session
1011
+ self?.urlSession?.invalidateAndCancel()
1012
+ self?.urlSession = self?.createURLSession()
1013
+ self?.stateQueue.sync { self?.isDownloading = false }
1014
+ OneKeyLog.info("BundleUpdate", "clearBundle: completed")
1015
+ }
1016
+ }
1017
+
1018
+ func clearAllJSBundleData() throws -> Promise<TestResult> {
1019
+ return Promise.async {
1020
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
1021
+ let bundleDir = BundleUpdateStore.bundleDir()
1022
+ if FileManager.default.fileExists(atPath: bundleDir) {
1023
+ try FileManager.default.removeItem(atPath: bundleDir)
1024
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted bundle dir")
1025
+ }
1026
+ let ud = UserDefaults.standard
1027
+ if let cbv = ud.string(forKey: "currentBundleVersion") {
1028
+ ud.removeObject(forKey: cbv)
1029
+ ud.removeObject(forKey: "currentBundleVersion")
1030
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: removed currentBundleVersion=\(cbv)")
1031
+ }
1032
+ ud.removeObject(forKey: "nativeVersion")
1033
+ ud.synchronize()
1034
+
1035
+ OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: completed successfully")
1036
+ return TestResult(success: true, message: "Successfully cleared all JS bundle data")
1037
+ }
1038
+ }
1039
+
1040
+ func getFallbackUpdateBundleData() throws -> Promise<[FallbackBundleInfo]> {
1041
+ return Promise.async {
1042
+ let data = BundleUpdateStore.readFallbackUpdateBundleDataFile()
1043
+ let result = data.compactMap { dict -> FallbackBundleInfo? in
1044
+ guard let appVersion = dict["appVersion"],
1045
+ let bundleVersion = dict["bundleVersion"],
1046
+ let signature = dict["signature"] else { return nil }
1047
+ return FallbackBundleInfo(appVersion: appVersion, bundleVersion: bundleVersion, signature: signature)
1048
+ }
1049
+ OneKeyLog.info("BundleUpdate", "getFallbackUpdateBundleData: found \(result.count) fallback entries")
1050
+ return result
1051
+ }
1052
+ }
1053
+
1054
+ func setCurrentUpdateBundleData(params: BundleSwitchParams) throws -> Promise<Void> {
1055
+ return Promise.async {
1056
+ let bundleVersion = "\(params.appVersion)-\(params.bundleVersion)"
1057
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to \(bundleVersion)")
1058
+
1059
+ // Verify the bundle directory actually exists
1060
+ let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(bundleVersion)
1061
+ guard FileManager.default.fileExists(atPath: bundleDirPath) else {
1062
+ OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: \(bundleDirPath)")
1063
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found"])
1064
+ }
1065
+
1066
+ // Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled)
1067
+ let devSettings = BundleUpdateStore.isDevSettingsEnabled()
1068
+ let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled()
1069
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle)")
1070
+ if !(devSettings && skipGPGToggle) {
1071
+ guard !params.signature.isEmpty,
1072
+ BundleUpdateStore.validateMetadataFileSha256(bundleVersion, signature: params.signature) else {
1073
+ OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed")
1074
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"])
1075
+ }
1076
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK")
1077
+ } else {
1078
+ OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)")
1079
+ }
1080
+
1081
+ let ud = UserDefaults.standard
1082
+ ud.set(bundleVersion, forKey: "currentBundleVersion")
1083
+ if !params.signature.isEmpty {
1084
+ BundleUpdateStore.writeSignatureFile(bundleVersion, signature: params.signature)
1085
+ }
1086
+ ud.synchronize()
1087
+ OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to \(bundleVersion)")
1088
+ }
1089
+ }
1090
+
1091
+ func getWebEmbedPath() throws -> String {
1092
+ let path = BundleUpdateStore.getWebEmbedPath()
1093
+ OneKeyLog.debug("BundleUpdate", "getWebEmbedPath: \(path)")
1094
+ return path
1095
+ }
1096
+
1097
+ func getWebEmbedPathAsync() throws -> Promise<String> {
1098
+ return Promise.async {
1099
+ let path = BundleUpdateStore.getWebEmbedPath()
1100
+ OneKeyLog.debug("BundleUpdate", "getWebEmbedPathAsync: \(path)")
1101
+ return path
1102
+ }
1103
+ }
1104
+
1105
+ func getJsBundlePath() throws -> Promise<String> {
1106
+ return Promise.async {
1107
+ let path = BundleUpdateStore.currentBundleMainJSBundle() ?? ""
1108
+ OneKeyLog.info("BundleUpdate", "getJsBundlePath: \(path.isEmpty ? "(empty/no bundle)" : path)")
1109
+ return path
1110
+ }
1111
+ }
1112
+
1113
+ func getNativeAppVersion() throws -> Promise<String> {
1114
+ return Promise.async {
1115
+ let version = BundleUpdateStore.getCurrentNativeVersion()
1116
+ OneKeyLog.info("BundleUpdate", "getNativeAppVersion: \(version)")
1117
+ return version
1118
+ }
1119
+ }
1120
+
1121
+ func testVerification() throws -> Promise<Bool> {
1122
+ return Promise.async {
1123
+ let testSignature = """
1124
+ -----BEGIN PGP SIGNED MESSAGE-----
1125
+ Hash: SHA256
1126
+
1127
+ {
1128
+ "fileName": "metadata.json",
1129
+ "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba",
1130
+ "size": 158590,
1131
+ "generatedAt": "2025-09-19T07:49:13.000Z"
1132
+ }
1133
+ -----BEGIN PGP SIGNATURE-----
1134
+
1135
+ iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr
1136
+ ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk
1137
+ CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA
1138
+ LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk
1139
+ HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH
1140
+ JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b
1141
+ 2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W
1142
+ okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA
1143
+ uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr
1144
+ /X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE
1145
+ o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM
1146
+ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
1147
+ 91Js0rQ=
1148
+ =A/Ii
1149
+ -----END PGP SIGNATURE-----
1150
+ """
1151
+ let result = BundleUpdateStore.verifyGPGAndExtractSha256(testSignature)
1152
+ let isValid = result == "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba"
1153
+ OneKeyLog.info("BundleUpdate", "testVerification: GPG verification result: \(isValid)")
1154
+ return isValid
1155
+ }
1156
+ }
1157
+
1158
+ func isBundleExists(appVersion: String, bundleVersion: String) throws -> Promise<Bool> {
1159
+ return Promise.async {
1160
+ let folderName = "\(appVersion)-\(bundleVersion)"
1161
+ let path = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
1162
+ let exists = FileManager.default.fileExists(atPath: path)
1163
+ OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=\(appVersion), bundleVersion=\(bundleVersion), exists=\(exists)")
1164
+ return exists
1165
+ }
1166
+ }
1167
+
1168
+ func verifyExtractedBundle(appVersion: String, bundleVersion: String) throws -> Promise<Void> {
1169
+ return Promise.async {
1170
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1171
+ let folderName = "\(appVersion)-\(bundleVersion)"
1172
+ let bundlePath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
1173
+ guard FileManager.default.fileExists(atPath: bundlePath) else {
1174
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: \(bundlePath)")
1175
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found"])
1176
+ }
1177
+ let metadataJsonPath = (bundlePath as NSString).appendingPathComponent("metadata.json")
1178
+ guard FileManager.default.fileExists(atPath: metadataJsonPath) else {
1179
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in \(bundlePath)")
1180
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "metadata.json not found"])
1181
+ }
1182
+ guard let data = FileManager.default.contents(atPath: metadataJsonPath),
1183
+ let metadata = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
1184
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: failed to parse metadata.json")
1185
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to parse metadata.json"])
1186
+ }
1187
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...")
1188
+ if !BundleUpdateStore.validateAllFilesInDir(bundlePath, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
1189
+ OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed")
1190
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "File integrity check failed"])
1191
+ }
1192
+ OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=\(metadata.count)")
1193
+ }
1194
+ }
1195
+
1196
+ func listLocalBundles() throws -> Promise<[LocalBundleInfo]> {
1197
+ return Promise.async {
1198
+ let bundleDir = BundleUpdateStore.bundleDir()
1199
+ let fm = FileManager.default
1200
+ guard let contents = try? fm.contentsOfDirectory(atPath: bundleDir) else {
1201
+ OneKeyLog.info("BundleUpdate", "listLocalBundles: bundle directory empty or not found")
1202
+ return []
1203
+ }
1204
+ var results: [LocalBundleInfo] = []
1205
+ for name in contents {
1206
+ let fullPath = (bundleDir as NSString).appendingPathComponent(name)
1207
+ var isDir: ObjCBool = false
1208
+ guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue else { continue }
1209
+ guard let lastDash = name.range(of: "-", options: .backwards),
1210
+ lastDash.lowerBound > name.startIndex else { continue }
1211
+ let appVersion = String(name[name.startIndex..<lastDash.lowerBound])
1212
+ let bundleVersion = String(name[lastDash.upperBound...])
1213
+ if !appVersion.isEmpty && !bundleVersion.isEmpty {
1214
+ results.append(LocalBundleInfo(appVersion: appVersion, bundleVersion: bundleVersion))
1215
+ }
1216
+ }
1217
+ OneKeyLog.info("BundleUpdate", "listLocalBundles: found \(results.count) bundles")
1218
+ return results
1219
+ }
1220
+ }
1221
+
1222
+ func listAscFiles() throws -> Promise<[AscFileInfo]> {
1223
+ return Promise.async {
1224
+ let ascDir = BundleUpdateStore.ascDir()
1225
+ let fm = FileManager.default
1226
+ guard let contents = try? fm.contentsOfDirectory(atPath: ascDir) else {
1227
+ OneKeyLog.info("BundleUpdate", "listAscFiles: asc directory empty or not found")
1228
+ return []
1229
+ }
1230
+ var results: [AscFileInfo] = []
1231
+ for name in contents {
1232
+ let fullPath = (ascDir as NSString).appendingPathComponent(name)
1233
+ var isDir: ObjCBool = false
1234
+ guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), !isDir.boolValue else { continue }
1235
+ let attrs = try? fm.attributesOfItem(atPath: fullPath)
1236
+ let fileSize = Double((attrs?[.size] as? UInt64) ?? 0)
1237
+ results.append(AscFileInfo(fileName: name, filePath: fullPath, fileSize: fileSize))
1238
+ }
1239
+ OneKeyLog.info("BundleUpdate", "listAscFiles: found \(results.count) files")
1240
+ return results
1241
+ }
1242
+ }
1243
+
1244
+ func getSha256FromFilePath(filePath: String) throws -> Promise<String> {
1245
+ return Promise.async {
1246
+ OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: filePath=\(filePath)")
1247
+ guard !filePath.isEmpty else {
1248
+ OneKeyLog.warn("BundleUpdate", "getSha256FromFilePath: empty filePath")
1249
+ return ""
1250
+ }
1251
+
1252
+ // Restrict to bundle-related directories only
1253
+ let resolvedPath = (filePath as NSString).resolvingSymlinksInPath
1254
+ let bundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
1255
+ let downloadDir = (BundleUpdateStore.downloadBundleDir() as NSString).resolvingSymlinksInPath
1256
+ guard resolvedPath.hasPrefix(bundleDir) || resolvedPath.hasPrefix(downloadDir) else {
1257
+ OneKeyLog.error("BundleUpdate", "getSha256FromFilePath: path outside allowed directories: \(resolvedPath)")
1258
+ throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "File path outside allowed bundle directories"])
1259
+ }
1260
+
1261
+ let sha256 = BundleUpdateStore.calculateSHA256(filePath) ?? ""
1262
+ OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: sha256=\(sha256.isEmpty ? "(empty)" : String(sha256.prefix(16)) + "...")")
1263
+ return sha256
1264
+ }
1265
+ }
1266
+
1267
+ func testDeleteJsBundle(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
1268
+ return Promise.async {
1269
+ OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1270
+ let folderName = "\(appVersion)-\(bundleVersion)"
1271
+ let jsBundlePath = (BundleUpdateStore.bundleDir() as NSString)
1272
+ .appendingPathComponent(folderName)
1273
+ let path = (jsBundlePath as NSString).appendingPathComponent("main.jsbundle.hbc")
1274
+
1275
+ if FileManager.default.fileExists(atPath: path) {
1276
+ try FileManager.default.removeItem(atPath: path)
1277
+ OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: deleted \(path)")
1278
+ return TestResult(success: true, message: "Deleted jsBundle: \(path)")
1279
+ }
1280
+ OneKeyLog.warn("BundleUpdate", "testDeleteJsBundle: file not found: \(path)")
1281
+ return TestResult(success: false, message: "jsBundle not found: \(path)")
1282
+ }
1283
+ }
1284
+
1285
+ func testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
1286
+ return Promise.async {
1287
+ OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1288
+ let folderName = "\(appVersion)-\(bundleVersion)"
1289
+ let dirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
1290
+
1291
+ if FileManager.default.fileExists(atPath: dirPath) {
1292
+ try FileManager.default.removeItem(atPath: dirPath)
1293
+ OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: deleted \(dirPath)")
1294
+ return TestResult(success: true, message: "Deleted js runtime directory: \(dirPath)")
1295
+ }
1296
+ OneKeyLog.warn("BundleUpdate", "testDeleteJsRuntimeDir: directory not found: \(dirPath)")
1297
+ return TestResult(success: false, message: "js runtime directory not found: \(dirPath)")
1298
+ }
1299
+ }
1300
+
1301
+ func testDeleteMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
1302
+ return Promise.async {
1303
+ OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1304
+ let folderName = "\(appVersion)-\(bundleVersion)"
1305
+ let metadataPath = (BundleUpdateStore.bundleDir() as NSString)
1306
+ .appendingPathComponent(folderName)
1307
+ let path = (metadataPath as NSString).appendingPathComponent("metadata.json")
1308
+
1309
+ if FileManager.default.fileExists(atPath: path) {
1310
+ try FileManager.default.removeItem(atPath: path)
1311
+ OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: deleted \(path)")
1312
+ return TestResult(success: true, message: "Deleted metadata.json: \(path)")
1313
+ }
1314
+ OneKeyLog.warn("BundleUpdate", "testDeleteMetadataJson: file not found: \(path)")
1315
+ return TestResult(success: false, message: "metadata.json not found: \(path)")
1316
+ }
1317
+ }
1318
+
1319
+ func testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
1320
+ return Promise.async {
1321
+ OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
1322
+ let folderName = "\(appVersion)-\(bundleVersion)"
1323
+ let jsRuntimeDir = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
1324
+ let metadataPath = (jsRuntimeDir as NSString).appendingPathComponent("metadata.json")
1325
+
1326
+ if !FileManager.default.fileExists(atPath: jsRuntimeDir) {
1327
+ try FileManager.default.createDirectory(atPath: jsRuntimeDir, withIntermediateDirectories: true)
1328
+ }
1329
+
1330
+ let emptyJson: [String: Any] = [:]
1331
+ let data = try JSONSerialization.data(withJSONObject: emptyJson, options: .prettyPrinted)
1332
+ try data.write(to: URL(fileURLWithPath: metadataPath), options: .atomic)
1333
+
1334
+ OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: created \(metadataPath)")
1335
+ return TestResult(success: true, message: "Created empty metadata.json: \(metadataPath)")
1336
+ }
1337
+ }
1338
+ }