@capgo/capacitor-updater 7.50.2 → 7.51.16
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/CapgoCapacitorUpdater.podspec +0 -2
- package/Package.swift +4 -5
- package/README.md +53 -48
- package/android/build.gradle +6 -6
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +427 -116
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1108 -182
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +513 -201
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +115 -133
- package/dist/docs.json +32 -8
- package/dist/esm/definitions.d.ts +41 -17
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +806 -92
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1020 -270
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- package/package.json +13 -8
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -141,8 +141,24 @@ public struct CryptoCipher {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/// 256 KiB: one size for checksum, copy, and decode.
|
|
145
|
+
/// 64 workers * 256 KiB = 16 MiB for one buffer; AES/Brotli hold two (~32 MiB).
|
|
146
|
+
static let ioBufferBytesValue = 256 * 1024
|
|
147
|
+
|
|
148
|
+
static func ioBufferBytes() -> Int {
|
|
149
|
+
return ioBufferBytesValue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static func checksumBufferBytes() -> Int {
|
|
153
|
+
return ioBufferBytesValue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static func copyBufferBytes() -> Int {
|
|
157
|
+
return ioBufferBytesValue
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
public static func calcChecksum(filePath: URL) -> String {
|
|
145
|
-
let bufferSize =
|
|
161
|
+
let bufferSize = checksumBufferBytes()
|
|
146
162
|
var sha256 = SHA256()
|
|
147
163
|
|
|
148
164
|
do {
|
|
@@ -182,8 +198,7 @@ public struct CryptoCipher {
|
|
|
182
198
|
}
|
|
183
199
|
}) {}
|
|
184
200
|
|
|
185
|
-
|
|
186
|
-
return digest.compactMap { String(format: "%02x", $0) }.joined()
|
|
201
|
+
return hexString(from: sha256)
|
|
187
202
|
} catch {
|
|
188
203
|
logger.error("Cannot calculate checksum")
|
|
189
204
|
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
@@ -191,6 +206,32 @@ public struct CryptoCipher {
|
|
|
191
206
|
}
|
|
192
207
|
}
|
|
193
208
|
|
|
209
|
+
final class RunningChecksum {
|
|
210
|
+
private var sha256 = SHA256()
|
|
211
|
+
|
|
212
|
+
func update(_ data: Data) {
|
|
213
|
+
guard !data.isEmpty else {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
sha256.update(data: data)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func hex() -> String {
|
|
220
|
+
CryptoCipher.hexString(from: sha256)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static func hexString(from sha256: SHA256) -> String {
|
|
225
|
+
var copy = sha256
|
|
226
|
+
return copy.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static func shortPathKey(_ fileName: String) -> String {
|
|
230
|
+
var sha256 = SHA256()
|
|
231
|
+
sha256.update(data: Data(fileName.utf8))
|
|
232
|
+
return String(hexString(from: sha256).prefix(16))
|
|
233
|
+
}
|
|
234
|
+
|
|
194
235
|
public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
|
|
195
236
|
if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
|
|
196
237
|
logger.info("Encryption not set, no public key or session, ignored")
|
|
@@ -243,42 +284,19 @@ public struct CryptoCipher {
|
|
|
243
284
|
|
|
244
285
|
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger)
|
|
245
286
|
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
encryptedData = try Data(contentsOf: filePath)
|
|
249
|
-
} catch {
|
|
250
|
-
logger.error("Failed to read encrypted data")
|
|
251
|
-
logger.debug("Error: \(error)")
|
|
252
|
-
throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if encryptedData.isEmpty {
|
|
287
|
+
let encryptedSize = (try FileManager.default.attributesOfItem(atPath: filePath.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
288
|
+
if encryptedSize == 0 {
|
|
256
289
|
logger.error("Encrypted file data is empty")
|
|
257
290
|
throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
|
|
258
291
|
}
|
|
259
292
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if decryptedData.isEmpty {
|
|
293
|
+
try aesPrivateKey.decrypt(from: filePath, to: filePath)
|
|
294
|
+
let decryptedSize = (try FileManager.default.attributesOfItem(atPath: filePath.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
295
|
+
if decryptedSize == 0 {
|
|
266
296
|
logger.error("Decrypted data is empty")
|
|
267
297
|
throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
|
|
268
298
|
}
|
|
269
299
|
|
|
270
|
-
do {
|
|
271
|
-
try decryptedData.write(to: filePath, options: .atomic)
|
|
272
|
-
if !FileManager.default.fileExists(atPath: filePath.path) {
|
|
273
|
-
logger.error("File was not created after write")
|
|
274
|
-
throw NSError(domain: "File write failed", code: 8, userInfo: nil)
|
|
275
|
-
}
|
|
276
|
-
} catch {
|
|
277
|
-
logger.error("Error writing decrypted file")
|
|
278
|
-
logger.debug("Error: \(error)")
|
|
279
|
-
throw error
|
|
280
|
-
}
|
|
281
|
-
|
|
282
300
|
} catch {
|
|
283
301
|
logger.error("File decryption failed")
|
|
284
302
|
throw CustomError.cannotDecode
|
|
@@ -1,274 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
///
|
|
7
|
-
/// Constants
|
|
8
|
-
///
|
|
9
|
-
private enum RSAConstants {
|
|
10
|
-
static let rsaKeySizeInBits: NSNumber = 2048
|
|
11
|
-
static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
|
|
12
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import os.log
|
|
16
10
|
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
11
|
+
/// Capgo v2 uses Node's privateEncrypt with PKCS#1 v1.5 type-1 padding for
|
|
12
|
+
/// checksums and session keys. Recover the signed payload using Apple's RSA
|
|
13
|
+
/// implementation instead of custom modular exponentiation.
|
|
20
14
|
public struct RSAPublicKey {
|
|
21
|
-
private let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
///
|
|
28
|
-
/// Takes the data and uses the public key to decrypt it.
|
|
29
|
-
/// Returns the decrypted data.
|
|
30
|
-
///
|
|
31
|
-
public func decrypt(data: Data) -> Data? {
|
|
32
|
-
return manualKey.decrypt(data)
|
|
33
|
-
}
|
|
15
|
+
private let key: SecKey
|
|
16
|
+
private static let keyLock = NSLock()
|
|
17
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
18
|
+
// One public key only: bounded storage, invalid/rotated keys never fall back
|
|
19
|
+
// to a cached key with different material. SecKey is immutable after import.
|
|
20
|
+
private static var cachedKey: (der: Data, key: SecKey)?
|
|
34
21
|
|
|
35
|
-
///
|
|
36
|
-
/// Allows you to load an RSA public key (i.e. one downloaded from the net).
|
|
37
|
-
///
|
|
38
22
|
public static func load(rsaPublicKey: String) -> RSAPublicKey? {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
pubKey = pubKey.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
42
|
-
pubKey = pubKey.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
43
|
-
pubKey = pubKey.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
44
|
-
pubKey = pubKey.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
45
|
-
pubKey = pubKey.replacingOccurrences(of: "\\n+", with: "", options: .regularExpression)
|
|
46
|
-
pubKey = pubKey.replacingOccurrences(of: "\n", with: "")
|
|
47
|
-
pubKey = pubKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
48
|
-
|
|
49
|
-
do {
|
|
50
|
-
guard let rsaPublicKeyData: Data = Data(base64Encoded: String(pubKey)) else {
|
|
51
|
-
throw CustomError.cannotDecode
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Try parsing as PKCS#1
|
|
55
|
-
if let manualKey = ManualRSAPublicKey.fromPKCS1(rsaPublicKeyData) {
|
|
56
|
-
return RSAPublicKey(manualKey: manualKey)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Most common public exponent is 65537 (0x010001)
|
|
60
|
-
let commonExponent = Data([0x01, 0x00, 0x01]) // 65537 in big-endian
|
|
61
|
-
|
|
62
|
-
// Assume the entire key data is the modulus
|
|
63
|
-
let lastResortKey = ManualRSAPublicKey(modulus: rsaPublicKeyData, exponent: commonExponent)
|
|
64
|
-
return RSAPublicKey(manualKey: lastResortKey)
|
|
65
|
-
} catch {
|
|
66
|
-
return nil
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Manual RSA Public Key Implementation using the BigInt library
|
|
72
|
-
struct ManualRSAPublicKey {
|
|
73
|
-
let modulus: BigInt
|
|
74
|
-
let exponent: BigInt
|
|
75
|
-
|
|
76
|
-
init(modulus: Data, exponent: Data) {
|
|
77
|
-
// Create positive BigInts from Data
|
|
78
|
-
let modulusBytes = [UInt8](modulus)
|
|
79
|
-
var modulusValue = BigUInt(0)
|
|
80
|
-
for byte in modulusBytes {
|
|
81
|
-
modulusValue = (modulusValue << 8) | BigUInt(byte)
|
|
82
|
-
}
|
|
83
|
-
self.modulus = BigInt(modulusValue)
|
|
84
|
-
let exponentBytes = [UInt8](exponent)
|
|
85
|
-
var exponentValue = BigUInt(0)
|
|
86
|
-
for byte in exponentBytes {
|
|
87
|
-
exponentValue = (exponentValue << 8) | BigUInt(byte)
|
|
88
|
-
}
|
|
89
|
-
self.exponent = BigInt(exponentValue)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Parse PKCS#1 format public key
|
|
93
|
-
static func fromPKCS1(_ publicKeyData: Data) -> ManualRSAPublicKey? {
|
|
94
|
-
// Parse ASN.1 DER encoded RSA public key
|
|
95
|
-
// Format: RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
|
96
|
-
|
|
97
|
-
guard publicKeyData.count > 0 else {
|
|
98
|
-
return nil
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let bytes = [UInt8](publicKeyData)
|
|
102
|
-
|
|
103
|
-
// Check for sequence tag (0x30)
|
|
104
|
-
guard bytes[0] == 0x30 else {
|
|
105
|
-
// Try direct modulus/exponent approach as fallback
|
|
106
|
-
if publicKeyData.count >= 3 {
|
|
107
|
-
// Assume this is a raw RSA public key with modulus + exponent
|
|
108
|
-
// Most common: modulus is 256 bytes (2048 bits), exponent is 3 bytes (0x010001 = 65537)
|
|
109
|
-
let modulusSize = publicKeyData.count - 3
|
|
110
|
-
if modulusSize > 0 {
|
|
111
|
-
let modulusData = publicKeyData.prefix(modulusSize)
|
|
112
|
-
let exponentData = publicKeyData.suffix(3)
|
|
113
|
-
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return nil
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
var index = 1
|
|
121
|
-
|
|
122
|
-
// Skip length
|
|
123
|
-
if bytes[index] & 0x80 != 0 {
|
|
124
|
-
let lenBytes = Int(bytes[index] & 0x7F)
|
|
125
|
-
if (index + 1 + lenBytes) >= bytes.count {
|
|
126
|
-
return nil
|
|
127
|
-
}
|
|
128
|
-
index += 1 + lenBytes
|
|
129
|
-
} else {
|
|
130
|
-
index += 1
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check for INTEGER tag for modulus (0x02)
|
|
134
|
-
if index >= bytes.count {
|
|
135
|
-
return nil
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
guard bytes[index] == 0x02 else {
|
|
139
|
-
return nil
|
|
140
|
-
}
|
|
141
|
-
index += 1
|
|
142
|
-
|
|
143
|
-
// Get modulus length
|
|
144
|
-
if index >= bytes.count {
|
|
145
|
-
return nil
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
var modulusLength = 0
|
|
149
|
-
if bytes[index] & 0x80 != 0 {
|
|
150
|
-
let lenBytes = Int(bytes[index] & 0x7F)
|
|
151
|
-
if (index + 1 + lenBytes) >= bytes.count {
|
|
152
|
-
return nil
|
|
153
|
-
}
|
|
154
|
-
index += 1
|
|
155
|
-
for i in 0..<lenBytes {
|
|
156
|
-
modulusLength = (modulusLength << 8) | Int(bytes[index + i])
|
|
157
|
-
}
|
|
158
|
-
index += lenBytes
|
|
159
|
-
} else {
|
|
160
|
-
modulusLength = Int(bytes[index])
|
|
161
|
-
index += 1
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Skip any leading zero in modulus (for unsigned integer)
|
|
165
|
-
if index < bytes.count && bytes[index] == 0x00 {
|
|
166
|
-
index += 1
|
|
167
|
-
modulusLength -= 1
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Extract modulus
|
|
171
|
-
if (index + modulusLength) > bytes.count {
|
|
172
|
-
return nil
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
let modulusData = Data(bytes[index..<(index + modulusLength)])
|
|
176
|
-
index += modulusLength
|
|
177
|
-
|
|
178
|
-
// Check for INTEGER tag for exponent
|
|
179
|
-
if index >= bytes.count {
|
|
23
|
+
guard let der = pemToDer(rsaPublicKey) else {
|
|
24
|
+
os_log("RSA key import failed: invalid PEM/base64", log: Self.log, type: .error)
|
|
180
25
|
return nil
|
|
181
26
|
}
|
|
182
27
|
|
|
183
|
-
|
|
28
|
+
keyLock.lock()
|
|
29
|
+
defer { keyLock.unlock() }
|
|
30
|
+
if let cached = cachedKey, cached.der == der {
|
|
31
|
+
return RSAPublicKey(key: cached.key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let attributes: [CFString: Any] = [
|
|
35
|
+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
|
36
|
+
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
37
|
+
]
|
|
38
|
+
var error: Unmanaged<CFError>?
|
|
39
|
+
guard let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error) else {
|
|
40
|
+
os_log(
|
|
41
|
+
"RSA key import failed: %{public}@",
|
|
42
|
+
log: Self.log,
|
|
43
|
+
type: .error,
|
|
44
|
+
String(describing: error?.takeRetainedValue())
|
|
45
|
+
)
|
|
184
46
|
return nil
|
|
185
47
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
48
|
+
guard CapgoRawRsa.supportsCapgoRecovery(key: key) else {
|
|
49
|
+
os_log(
|
|
50
|
+
"RSA key import failed: unsupported key (Capgo requires RSA-2048 with raw encryption)",
|
|
51
|
+
log: Self.log,
|
|
52
|
+
type: .error
|
|
53
|
+
)
|
|
190
54
|
return nil
|
|
191
55
|
}
|
|
56
|
+
cachedKey = (der, key)
|
|
57
|
+
return RSAPublicKey(key: key)
|
|
58
|
+
}
|
|
192
59
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for i in 0..<lenBytes {
|
|
201
|
-
exponentLength = (exponentLength << 8) | Int(bytes[index + i])
|
|
202
|
-
}
|
|
203
|
-
index += lenBytes
|
|
204
|
-
} else {
|
|
205
|
-
exponentLength = Int(bytes[index])
|
|
206
|
-
index += 1
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Extract exponent
|
|
210
|
-
if (index + exponentLength) > bytes.count {
|
|
60
|
+
public func decrypt(data: Data) -> Data? {
|
|
61
|
+
let blockSize = SecKeyGetBlockSize(key)
|
|
62
|
+
guard data.count == blockSize else { return nil }
|
|
63
|
+
// Raw public-key encryption is the same c^e mod n operation as Node's
|
|
64
|
+
// publicDecrypt. Do NOT use encryption PKCS#1 padding here: the recovered
|
|
65
|
+
// block already contains signature (type-1) padding, checked below.
|
|
66
|
+
guard let recovered = CapgoRawRsa.recoverSignatureBlock(key: key, data: data) else {
|
|
211
67
|
return nil
|
|
212
68
|
}
|
|
213
|
-
|
|
214
|
-
let exponentData = Data(bytes[index..<(index + exponentLength)])
|
|
215
|
-
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
69
|
+
return Self.unpadSignature(recovered, blockSize: blockSize)
|
|
216
70
|
}
|
|
217
71
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
72
|
+
static func unpadSignature(_ block: Data, blockSize: Int = 256) -> Data? {
|
|
73
|
+
let bytes = [UInt8](block)
|
|
74
|
+
// RFC 8017: 00 01, at least eight FF bytes, 00, then the payload.
|
|
75
|
+
guard bytes.count == blockSize, bytes[0] == 0, bytes[1] == 1 else { return nil }
|
|
76
|
+
var separator = 2
|
|
77
|
+
while separator < bytes.count && bytes[separator] == 0xff {
|
|
78
|
+
separator += 1
|
|
225
79
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// In Node.js:
|
|
229
|
-
// privateEncrypt uses the private key (d) to encrypt
|
|
230
|
-
// publicDecrypt uses the public key (e) to decrypt
|
|
231
|
-
// The operation we want is: ciphertext^e mod n
|
|
232
|
-
|
|
233
|
-
// RSA operation: c^e mod n
|
|
234
|
-
let decrypted = encrypted.manualPower(exponent, modulus: modulus)
|
|
235
|
-
|
|
236
|
-
// Convert to bytes with proper padding
|
|
237
|
-
guard let bigUIntValue = decrypted.magnitude as? BigUInt else {
|
|
80
|
+
guard separator >= 10, separator < bytes.count - 1, bytes[separator] == 0 else {
|
|
238
81
|
return nil
|
|
239
82
|
}
|
|
83
|
+
return Data(bytes[(separator + 1)...])
|
|
84
|
+
}
|
|
240
85
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Ensure we have at least 256 bytes (2048 bits) with leading zeros
|
|
251
|
-
let paddedBytes = [UInt8](repeating: 0, count: max(0, 256 - resultBytes.count)) + resultBytes
|
|
252
|
-
|
|
253
|
-
// For PKCS1 padding from Node.js privateEncrypt, the format is:
|
|
254
|
-
// 0x00 || 0x01 || PS || 0x00 || actual data
|
|
255
|
-
// where PS is a string of 0xFF bytes
|
|
256
|
-
|
|
257
|
-
// Check for privateEncrypt padding format (0x00 || 0x01 || PS || 0x00)
|
|
258
|
-
var startIndex = 0
|
|
259
|
-
if paddedBytes.count > 2 && paddedBytes[0] == 0x00 && paddedBytes[1] == 0x01 {
|
|
260
|
-
for i in 2..<paddedBytes.count {
|
|
261
|
-
if paddedBytes[i] == 0x00 {
|
|
262
|
-
startIndex = i + 1
|
|
263
|
-
break
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if startIndex < paddedBytes.count {
|
|
268
|
-
let result = Data(paddedBytes[startIndex...])
|
|
269
|
-
return result
|
|
270
|
-
} else {
|
|
271
|
-
return Data(paddedBytes)
|
|
272
|
-
}
|
|
86
|
+
private static func pemToDer(_ pem: String) -> Data? {
|
|
87
|
+
let encoded = pem
|
|
88
|
+
.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
89
|
+
.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
90
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
91
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
92
|
+
.replacingOccurrences(of: "\\n", with: "")
|
|
93
|
+
.components(separatedBy: .whitespacesAndNewlines).joined()
|
|
94
|
+
return Data(base64Encoded: encoded)
|
|
273
95
|
}
|
|
274
96
|
}
|
|
@@ -92,18 +92,26 @@ final class ThreeFingerPinchGestureRecognizer: UIGestureRecognizer {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
extension UIApplication {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if let nav =
|
|
95
|
+
public class func topViewController(_ base: UIViewController? = nil) -> UIViewController? {
|
|
96
|
+
let resolvedBase = base ?? keyWindowRootViewController
|
|
97
|
+
if let nav = resolvedBase as? UINavigationController {
|
|
98
98
|
return topViewController(nav.visibleViewController)
|
|
99
99
|
}
|
|
100
|
-
if let tab =
|
|
100
|
+
if let tab = resolvedBase as? UITabBarController, let selected = tab.selectedViewController {
|
|
101
101
|
return topViewController(selected)
|
|
102
102
|
}
|
|
103
|
-
if let presented =
|
|
103
|
+
if let presented = resolvedBase?.presentedViewController {
|
|
104
104
|
return topViewController(presented)
|
|
105
105
|
}
|
|
106
|
-
return
|
|
106
|
+
return resolvedBase
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static var keyWindowRootViewController: UIViewController? {
|
|
110
|
+
let scenes = shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
111
|
+
let foregroundScenes = scenes.filter { $0.activationState == .foregroundActive }
|
|
112
|
+
let candidates = foregroundScenes.isEmpty ? scenes : foregroundScenes
|
|
113
|
+
let window = candidates.flatMap(\.windows).first(where: \.isKeyWindow) ?? candidates.first?.windows.first
|
|
114
|
+
return window?.rootViewController
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -184,6 +192,10 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
extension UIWindow {
|
|
195
|
+
private var menuHostViewController: UIViewController? {
|
|
196
|
+
UIApplication.topViewController(rootViewController)
|
|
197
|
+
}
|
|
198
|
+
|
|
187
199
|
override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
|
188
200
|
if motion == .motionShake {
|
|
189
201
|
guard let bridgeViewController = rootViewController as? CAPBridgeViewController,
|
|
@@ -230,7 +242,7 @@ extension UIWindow {
|
|
|
230
242
|
@discardableResult
|
|
231
243
|
private func showDefaultMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
|
|
232
244
|
// Prevent multiple alerts from showing
|
|
233
|
-
guard let topVC =
|
|
245
|
+
guard let topVC = self.menuHostViewController else {
|
|
234
246
|
return false
|
|
235
247
|
}
|
|
236
248
|
if topVC.isKind(of: UIAlertController.self) {
|
|
@@ -365,7 +377,7 @@ extension UIWindow {
|
|
|
365
377
|
})
|
|
366
378
|
|
|
367
379
|
DispatchQueue.main.async {
|
|
368
|
-
if let topVC =
|
|
380
|
+
if let topVC = self.menuHostViewController {
|
|
369
381
|
topVC.present(alertShake, animated: true)
|
|
370
382
|
}
|
|
371
383
|
}
|
|
@@ -388,7 +400,7 @@ extension UIWindow {
|
|
|
388
400
|
}
|
|
389
401
|
|
|
390
402
|
private func showPreviewSelector(plugin: CapacitorUpdaterPlugin) {
|
|
391
|
-
guard let topVC =
|
|
403
|
+
guard let topVC = self.menuHostViewController else {
|
|
392
404
|
return
|
|
393
405
|
}
|
|
394
406
|
if topVC.isKind(of: UIAlertController.self) {
|
|
@@ -456,7 +468,7 @@ extension UIWindow {
|
|
|
456
468
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
457
469
|
|
|
458
470
|
DispatchQueue.main.async {
|
|
459
|
-
if let topVC =
|
|
471
|
+
if let topVC = self.menuHostViewController {
|
|
460
472
|
topVC.present(alert, animated: true)
|
|
461
473
|
}
|
|
462
474
|
}
|
|
@@ -479,7 +491,7 @@ extension UIWindow {
|
|
|
479
491
|
}
|
|
480
492
|
|
|
481
493
|
DispatchQueue.main.async {
|
|
482
|
-
if let topVC =
|
|
494
|
+
if let topVC = self.menuHostViewController {
|
|
483
495
|
topVC.present(alert, animated: true)
|
|
484
496
|
}
|
|
485
497
|
}
|
|
@@ -498,7 +510,7 @@ extension UIWindow {
|
|
|
498
510
|
@discardableResult
|
|
499
511
|
private func showChannelSelector(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
|
|
500
512
|
// Prevent multiple alerts from showing
|
|
501
|
-
guard let topVC =
|
|
513
|
+
guard let topVC = self.menuHostViewController else {
|
|
502
514
|
return false
|
|
503
515
|
}
|
|
504
516
|
if topVC.isKind(of: UIAlertController.self) {
|
|
@@ -582,7 +594,7 @@ extension UIWindow {
|
|
|
582
594
|
}
|
|
583
595
|
|
|
584
596
|
DispatchQueue.main.async {
|
|
585
|
-
if let topVC =
|
|
597
|
+
if let topVC = self.menuHostViewController {
|
|
586
598
|
topVC.present(alert, animated: true)
|
|
587
599
|
}
|
|
588
600
|
}
|
|
@@ -627,7 +639,7 @@ extension UIWindow {
|
|
|
627
639
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
628
640
|
|
|
629
641
|
DispatchQueue.main.async {
|
|
630
|
-
if let topVC =
|
|
642
|
+
if let topVC = self.menuHostViewController {
|
|
631
643
|
topVC.present(alert, animated: true)
|
|
632
644
|
}
|
|
633
645
|
}
|
|
@@ -649,7 +661,7 @@ extension UIWindow {
|
|
|
649
661
|
])
|
|
650
662
|
|
|
651
663
|
DispatchQueue.main.async {
|
|
652
|
-
if let topVC =
|
|
664
|
+
if let topVC = self.menuHostViewController {
|
|
653
665
|
topVC.present(progressAlert, animated: true) {
|
|
654
666
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
655
667
|
// Set the channel - respect plugin's allowSetDefaultChannel config
|
|
@@ -668,6 +680,14 @@ extension UIWindow {
|
|
|
668
680
|
}
|
|
669
681
|
return
|
|
670
682
|
}
|
|
683
|
+
guard plugin.persistDefaultChannelStateFromDefaults() else {
|
|
684
|
+
DispatchQueue.main.async {
|
|
685
|
+
progressAlert.dismiss(animated: true) {
|
|
686
|
+
self.showError(message: "Channel set to \(name), but local persistence failed.", plugin: plugin)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return
|
|
690
|
+
}
|
|
671
691
|
|
|
672
692
|
// Update progress message
|
|
673
693
|
DispatchQueue.main.async {
|
|
@@ -718,8 +738,12 @@ extension UIWindow {
|
|
|
718
738
|
return
|
|
719
739
|
}
|
|
720
740
|
|
|
721
|
-
// Check if there's an actual update available
|
|
722
|
-
|
|
741
|
+
// Check if there's an actual update available. A manifest-only
|
|
742
|
+
// response legitimately has no URL (the files come from the
|
|
743
|
+
// manifest, not a zip), so only report "already on latest" when
|
|
744
|
+
// the URL is empty AND there is no manifest to download from.
|
|
745
|
+
let hasManifest = !(latest.manifest?.isEmpty ?? true)
|
|
746
|
+
if latestKind == "up_to_date" || (latest.url.isEmpty && !hasManifest) {
|
|
723
747
|
DispatchQueue.main.async {
|
|
724
748
|
progressAlert.dismiss(animated: true) {
|
|
725
749
|
self.showSuccess(message: "Channel set to \(name). Already on latest version.", plugin: plugin)
|
|
@@ -794,7 +818,7 @@ extension UIWindow {
|
|
|
794
818
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
795
819
|
|
|
796
820
|
DispatchQueue.main.async {
|
|
797
|
-
if let topVC =
|
|
821
|
+
if let topVC = self.menuHostViewController {
|
|
798
822
|
topVC.present(alert, animated: true)
|
|
799
823
|
}
|
|
800
824
|
}
|
|
@@ -806,7 +830,7 @@ extension UIWindow {
|
|
|
806
830
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
807
831
|
|
|
808
832
|
DispatchQueue.main.async {
|
|
809
|
-
if let topVC =
|
|
833
|
+
if let topVC = self.menuHostViewController {
|
|
810
834
|
topVC.present(alert, animated: true)
|
|
811
835
|
}
|
|
812
836
|
}
|
|
@@ -832,7 +856,7 @@ extension UIWindow {
|
|
|
832
856
|
})
|
|
833
857
|
|
|
834
858
|
DispatchQueue.main.async {
|
|
835
|
-
if let topVC =
|
|
859
|
+
if let topVC = self.menuHostViewController {
|
|
836
860
|
topVC.present(alert, animated: true)
|
|
837
861
|
}
|
|
838
862
|
}
|