@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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ReactNativeBundleUpdate.podspec +34 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +139 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +1409 -0
- package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt +24 -0
- package/ios/Frameworks/Gopenpgp.xcframework/Info.plist +52 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap +13 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp +0 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h +96 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h +197 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h +1963 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h +23 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h +59 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h +252 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h +107 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h +29 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h +35 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist +20 -0
- package/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap +13 -0
- package/ios/ReactNativeBundleUpdate.swift +1338 -0
- package/lib/module/ReactNativeBundleUpdate.nitro.js +4 -0
- package/lib/module/ReactNativeBundleUpdate.nitro.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +101 -0
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JAscFileInfo.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +77 -0
- package/nitrogen/generated/android/c++/JBundleDownloadEvent.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleDownloadParams.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +69 -0
- package/nitrogen/generated/android/c++/JBundleSwitchParams.hpp +65 -0
- package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +73 -0
- package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +69 -0
- package/nitrogen/generated/android/c++/JFallbackBundleInfo.hpp +65 -0
- package/nitrogen/generated/android/c++/JFunc_void_BundleDownloadEvent.hpp +78 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.cpp +486 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeBundleUpdateSpec.hpp +89 -0
- package/nitrogen/generated/android/c++/JLocalBundleInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JTestResult.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/AscFileInfo.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadEvent.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleSwitchParams.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/FallbackBundleInfo.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/Func_void_BundleDownloadEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/HybridReactNativeBundleUpdateSpec.kt +159 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/LocalBundleInfo.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/TestResult.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/reactnativebundleupdateOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativebundleupdate+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativebundleupdate+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativebundleupdateOnLoad.cpp +46 -0
- package/nitrogen/generated/android/reactnativebundleupdateOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.cpp +113 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Bridge.hpp +513 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdate-Swift-Cxx-Umbrella.hpp +83 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeBundleUpdateAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeBundleUpdateSpecSwift.hpp +304 -0
- package/nitrogen/generated/ios/swift/AscFileInfo.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +91 -0
- package/nitrogen/generated/ios/swift/BundleDownloadEvent.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleDownloadParams.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleInstallParams.swift +69 -0
- package/nitrogen/generated/ios/swift/BundleSwitchParams.swift +58 -0
- package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +80 -0
- package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +69 -0
- package/nitrogen/generated/ios/swift/FallbackBundleInfo.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_BundleDownloadEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_BundleDownloadResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_TestResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_AscFileInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_FallbackBundleInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_LocalBundleInfo_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec.swift +80 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeBundleUpdateSpec_cxx.swift +595 -0
- package/nitrogen/generated/ios/swift/LocalBundleInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/TestResult.swift +47 -0
- package/nitrogen/generated/shared/c++/AscFileInfo.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +95 -0
- package/nitrogen/generated/shared/c++/BundleDownloadEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleDownloadParams.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/BundleSwitchParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +91 -0
- package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/FallbackBundleInfo.hpp +83 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.cpp +45 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeBundleUpdateSpec.hpp +124 -0
- package/nitrogen/generated/shared/c++/LocalBundleInfo.hpp +79 -0
- package/nitrogen/generated/shared/c++/TestResult.hpp +79 -0
- package/package.json +169 -0
- package/src/ReactNativeBundleUpdate.nitro.ts +143 -0
- 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
|
+
}
|