@capgo/capacitor-updater 7.0.36 → 7.0.38

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.
@@ -16,5 +16,6 @@ Pod::Spec.new do |s|
16
16
  s.dependency 'SSZipArchive', '2.4.3'
17
17
  s.dependency 'Alamofire', '5.10.2'
18
18
  s.dependency 'Version', '0.8.0'
19
+ s.dependency 'BigInt', '5.2.0'
19
20
  s.swift_version = '5.1'
20
21
  end
@@ -57,7 +57,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
57
57
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
58
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
59
 
60
- private final String PLUGIN_VERSION = "7.0.36";
60
+ private final String PLUGIN_VERSION = "7.0.38";
61
61
  private static final String DELAY_CONDITION_PREFERENCES = "";
62
62
 
63
63
  private SharedPreferences.Editor editor;
@@ -192,24 +192,6 @@ public class CryptoCipherV2 {
192
192
  }
193
193
  }
194
194
 
195
- public static String decryptChecksum(String checksum, String publicKey, String version) throws IOException {
196
- if (publicKey.isEmpty()) {
197
- Log.e(CapacitorUpdater.TAG, "The public key is empty");
198
- return checksum;
199
- }
200
- try {
201
- byte[] checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
202
- PublicKey pKey = CryptoCipherV2.stringToPublicKey(publicKey);
203
- byte[] decryptedChecksum = CryptoCipherV2.decryptRSA(checksumBytes, pKey);
204
- // return Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
205
- String result = Base64.encodeToString(decryptedChecksum, Base64.DEFAULT);
206
- return result.replaceAll("\\s", ""); // Remove all whitespace, including newlines
207
- } catch (GeneralSecurityException e) {
208
- Log.e(CapacitorUpdater.TAG, "decryptChecksum fail: " + e.getMessage());
209
- throw new IOException("Decryption failed: " + e.getMessage());
210
- }
211
- }
212
-
213
195
  public static String calcChecksum(File file) {
214
196
  final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
215
197
  MessageDigest digest;
@@ -377,8 +377,16 @@ public class DownloadService extends Worker {
377
377
 
378
378
  Request request = new Request.Builder().url(downloadUrl).build();
379
379
 
380
+ // Check if file is a Brotli file
381
+ boolean isBrotli = targetFile.getName().endsWith(".br");
382
+
383
+ // Create final target file with .br extension removed if it's a Brotli file
384
+ File finalTargetFile = isBrotli
385
+ ? new File(targetFile.getParentFile(), targetFile.getName().substring(0, targetFile.getName().length() - 3))
386
+ : targetFile;
387
+
380
388
  // Create a temporary file for the compressed data
381
- File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".br");
389
+ File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
382
390
 
383
391
  try (Response response = client.newCall(request).execute()) {
384
392
  if (!response.isSuccessful()) {
@@ -405,21 +413,27 @@ public class DownloadService extends Worker {
405
413
  CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
406
414
  }
407
415
 
408
- // Use new decompression method
409
- byte[] compressedData = Files.readAllBytes(compressedFile.toPath());
410
- byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
411
- Files.write(targetFile.toPath(), decompressedData);
416
+ // Only decompress if file has .br extension
417
+ if (isBrotli) {
418
+ // Use new decompression method
419
+ byte[] compressedData = Files.readAllBytes(compressedFile.toPath());
420
+ byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
421
+ Files.write(finalTargetFile.toPath(), decompressedData);
422
+ } else {
423
+ // Just copy the file without decompression
424
+ Files.copy(compressedFile.toPath(), finalTargetFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
425
+ }
412
426
 
413
427
  // Delete the compressed file
414
428
  compressedFile.delete();
415
- String calculatedHash = CryptoCipherV2.calcChecksum(targetFile);
429
+ String calculatedHash = CryptoCipherV2.calcChecksum(finalTargetFile);
416
430
 
417
431
  // Verify checksum
418
432
  if (calculatedHash.equals(expectedHash)) {
419
433
  // Only cache if checksum is correct
420
- copyFile(targetFile, cacheFile);
434
+ copyFile(finalTargetFile, cacheFile);
421
435
  } else {
422
- targetFile.delete();
436
+ finalTargetFile.delete();
423
437
  throw new IOException(
424
438
  "Checksum verification failed for: " +
425
439
  downloadUrl +
@@ -0,0 +1,66 @@
1
+ import BigInt
2
+ import CommonCrypto
3
+ import CryptoKit
4
+
5
+ ///
6
+ /// Constants
7
+ ///
8
+ private enum AESConstants {
9
+ static let aesAlgorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
10
+ static let aesOptions: CCOptions = CCOptions(kCCOptionPKCS7Padding)
11
+ }
12
+
13
+ // We do all this stuff because ios is shit and open source libraries allow to do decryption with public key
14
+ // So we have to do it manually, while in nodejs or Java it's ok and done at language level.
15
+
16
+ ///
17
+ /// The AES key. Contains both the initialization vector and secret key.
18
+ ///
19
+ public struct AES128Key {
20
+ /// Initialization vector
21
+ private let iv: Data
22
+ private let aes128Key: Data
23
+ #if DEBUG
24
+ public var __debug_iv: Data { iv }
25
+ public var __debug_aes128Key: Data { aes128Key }
26
+ #endif
27
+ init(iv: Data, aes128Key: Data) {
28
+ self.iv = iv
29
+ self.aes128Key = aes128Key
30
+ }
31
+ ///
32
+ /// Takes the data and uses the private key to decrypt it. Will call `CCCrypt` in CommonCrypto
33
+ /// and provide it `ivData` for the initialization vector. Will use cipher block chaining (CBC) as
34
+ /// the mode of operation.
35
+ ///
36
+ /// Returns the decrypted data.
37
+ ///
38
+ public func decrypt(data: Data) -> Data? {
39
+ let encryptedData: UnsafePointer<UInt8> = (data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count)
40
+ let encryptedDataLength: Int = data.count
41
+
42
+ if let result: NSMutableData = NSMutableData(length: encryptedDataLength) {
43
+ let keyData: UnsafePointer<UInt8> = (self.aes128Key as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.aes128Key.count)
44
+ let keyLength: size_t = size_t(self.aes128Key.count)
45
+ let ivData: UnsafePointer<UInt8> = (iv as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.iv.count)
46
+
47
+ let decryptedData: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>(result.mutableBytes.assumingMemoryBound(to: UInt8.self))
48
+ let decryptedDataLength: size_t = size_t(result.length)
49
+
50
+ var decryptedLength: size_t = 0
51
+
52
+ let status: CCCryptorStatus = CCCrypt(CCOperation(kCCDecrypt), AESConstants.aesAlgorithm, AESConstants.aesOptions, keyData, keyLength, ivData, encryptedData, encryptedDataLength, decryptedData, decryptedDataLength, &decryptedLength)
53
+
54
+ if Int32(status) == Int32(kCCSuccess) {
55
+ result.length = Int(decryptedLength)
56
+ return result as Data
57
+ } else {
58
+ print("\(CapacitorUpdater.TAG) AES decryption failed with status: \(status)")
59
+ return nil
60
+ }
61
+ } else {
62
+ print("\(CapacitorUpdater.TAG) Failed to allocate memory for AES decryption")
63
+ return nil
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,55 @@
1
+ import BigInt
2
+
3
+ // Extension to serialize BigInt to bytes array
4
+ extension BigInt {
5
+ func serializeToBytes() -> [UInt8] {
6
+ let byteCount = (self.bitWidth + 7) / 8
7
+ var bytes = [UInt8](repeating: 0, count: byteCount)
8
+
9
+ var value = self
10
+ for i in 0..<byteCount {
11
+ bytes[byteCount - i - 1] = UInt8(truncatingIfNeeded: value & 0xFF)
12
+ value >>= 8
13
+ }
14
+
15
+ return bytes
16
+ }
17
+ }
18
+
19
+ // Add this custom power function to ensure safer handling of power operations
20
+
21
+ // Manual exponentiation using the square-and-multiply algorithm
22
+ // which is more efficient and avoids using the built-in functions that might handle BigInt differently
23
+ extension BigInt {
24
+ func manualPower(_ exponent: BigInt, modulus: BigInt) -> BigInt {
25
+ // Quick checks
26
+ if modulus == 0 {
27
+ return 0
28
+ }
29
+
30
+ if exponent == 0 {
31
+ return 1
32
+ }
33
+
34
+ guard let base = self.magnitude as? BigUInt,
35
+ let exp = exponent.magnitude as? BigUInt,
36
+ let mod = modulus.magnitude as? BigUInt else {
37
+ return 0
38
+ }
39
+
40
+ // Square and multiply algorithm for modular exponentiation
41
+ var result = BigUInt(1)
42
+ var x = base % mod
43
+ var e = exp
44
+
45
+ while e > 0 {
46
+ if e & 1 == 1 {
47
+ result = (result * x) % mod
48
+ }
49
+ x = (x * x) % mod
50
+ e >>= 1
51
+ }
52
+
53
+ return BigInt(result)
54
+ }
55
+ }
@@ -340,7 +340,7 @@ import UIKit
340
340
 
341
341
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
342
342
  do {
343
- fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey, version: version)
343
+ fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
344
344
  } catch {
345
345
  downloadError = error
346
346
  print("\(CapacitorUpdater.TAG) CryptoCipherV2.decryptChecksum error \(id) \(fileName) error: \(error)")
@@ -403,11 +403,18 @@ import UIKit
403
403
  try FileManager.default.removeItem(at: tempFile)
404
404
  }
405
405
 
406
- // Decompress the Brotli data
407
- guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
408
- throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
406
+ // Check if file has .br extension for Brotli decompression
407
+ let isBrotli = fileName.hasSuffix(".br")
408
+ let finalFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
409
+ let destFilePath = destFolder.appendingPathComponent(finalFileName)
410
+
411
+ if isBrotli {
412
+ // Decompress the Brotli data
413
+ guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
414
+ throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
415
+ }
416
+ finalData = decompressedData
409
417
  }
410
- finalData = decompressedData
411
418
 
412
419
  try finalData.write(to: destFilePath)
413
420
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
@@ -423,7 +430,7 @@ import UIKit
423
430
 
424
431
  completedFiles += 1
425
432
  self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
426
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
433
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded\(isBrotli ? ", decompressed" : "")\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
427
434
  } catch {
428
435
  downloadError = error
429
436
  NSLog("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error.localizedDescription)")
@@ -45,7 +45,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
45
45
  CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise)
46
46
  ]
47
47
  public var implementation = CapacitorUpdater()
48
- private let PLUGIN_VERSION: String = "7.0.36"
48
+ private let PLUGIN_VERSION: String = "7.0.38"
49
49
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
50
50
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
51
51
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -291,7 +291,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
291
291
  DispatchQueue.global(qos: .background).async {
292
292
  do {
293
293
  let next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
294
- checksum = try CryptoCipherV2.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey, version: version)
294
+ checksum = try CryptoCipherV2.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
295
295
  if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
296
296
  print("\(CapacitorUpdater.TAG) Error checksum", next.getChecksum(), checksum)
297
297
  self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
@@ -805,7 +805,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
805
805
  self.endBackGroundTaskWithNotif(msg: "Latest version is in error state. Aborting update.", latestVersionName: latestVersionName, current: current)
806
806
  return
807
807
  }
808
- res.checksum = try CryptoCipherV2.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey, version: latestVersionName)
808
+ res.checksum = try CryptoCipherV2.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
809
809
  if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
810
810
  print("\(CapacitorUpdater.TAG) Error checksum", next.getChecksum(), res.checksum)
811
811
  self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
@@ -5,235 +5,40 @@
5
5
  */
6
6
 
7
7
  import Foundation
8
- import CommonCrypto
9
8
  import CryptoKit
9
+ import BigInt
10
10
 
11
- ///
12
- /// Constants
13
- ///
14
- private enum CryptoCipherConstants {
15
- static let rsaKeySizeInBits: NSNumber = 2048
16
- static let aesAlgorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
17
- static let aesOptions: CCOptions = CCOptions(kCCOptionPKCS7Padding)
18
- static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
19
- }
20
- ///
21
- /// The AES key. Contains both the initialization vector and secret key.
22
- ///
23
- public struct AES128Key {
24
- /// Initialization vector
25
- private let iv: Data
26
- private let aes128Key: Data
27
- #if DEBUG
28
- public var __debug_iv: Data { iv }
29
- public var __debug_aes128Key: Data { aes128Key }
30
- #endif
31
- init(iv: Data, aes128Key: Data) {
32
- self.iv = iv
33
- self.aes128Key = aes128Key
34
- }
35
- ///
36
- /// Takes the data and uses the private key to decrypt it. Will call `CCCrypt` in CommonCrypto
37
- /// and provide it `ivData` for the initialization vector. Will use cipher block chaining (CBC) as
38
- /// the mode of operation.
39
- ///
40
- /// Returns the decrypted data.
41
- ///
42
- public func decrypt(data: Data) -> Data? {
43
- let encryptedData: UnsafePointer<UInt8> = (data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count)
44
- let encryptedDataLength: Int = data.count
45
-
46
- if let result: NSMutableData = NSMutableData(length: encryptedDataLength) {
47
- let keyData: UnsafePointer<UInt8> = (self.aes128Key as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.aes128Key.count)
48
- let keyLength: size_t = size_t(self.aes128Key.count)
49
- let ivData: UnsafePointer<UInt8> = (iv as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.iv.count)
50
-
51
- let decryptedData: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>(result.mutableBytes.assumingMemoryBound(to: UInt8.self))
52
- let decryptedDataLength: size_t = size_t(result.length)
53
-
54
- var decryptedLength: size_t = 0
55
-
56
- let status: CCCryptorStatus = CCCrypt(CCOperation(kCCDecrypt), CryptoCipherConstants.aesAlgorithm, CryptoCipherConstants.aesOptions, keyData, keyLength, ivData, encryptedData, encryptedDataLength, decryptedData, decryptedDataLength, &decryptedLength)
11
+ public struct CryptoCipherV2 {
57
12
 
58
- if Int32(status) == Int32(kCCSuccess) {
59
- result.length = Int(decryptedLength)
60
- return result as Data
61
- } else {
62
- return nil
63
- }
64
- } else {
65
- return nil
13
+ public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
14
+ if publicKey.isEmpty {
15
+ print("\(CapacitorUpdater.TAG) The public key is empty")
16
+ return checksum
66
17
  }
67
- }
68
- }
69
-
70
- ///
71
- /// The RSA public key.
72
- ///
73
- public struct RSAPublicKey {
74
- private let publicKey: SecKey
75
-
76
- #if DEBUG
77
- public var __debug_publicKey: SecKey { self.publicKey }
78
- #endif
79
-
80
- fileprivate init(publicKey: SecKey) {
81
- self.publicKey = publicKey
82
- }
83
-
84
- ///
85
- /// Takes the data and uses the public key to decrypt it.
86
- /// Returns the decrypted data.
87
- ///
88
- public func decrypt(data: Data) -> Data? {
89
18
  do {
90
- guard let decryptedData = RSAPublicKey.decryptWithRSAKey(data, rsaKeyRef: self.publicKey, padding: SecPadding()) else {
91
- throw CustomError.cannotDecryptSessionKey
92
- }
93
-
94
- return decryptedData
95
- } catch {
96
- print("Error decrypting data: \(error)")
97
- return nil
98
- }
99
- }
100
-
101
- ///
102
- /// Allows you to load an RSA public key (i.e. one downloaded from the net).
103
- ///
104
- public static func load(rsaPublicKey: String) -> RSAPublicKey? {
105
- var pubKey: String = rsaPublicKey
106
- pubKey = pubKey.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
107
- pubKey = pubKey.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
108
- pubKey = pubKey.replacingOccurrences(of: "\\n+", with: "", options: .regularExpression)
109
- pubKey = pubKey.trimmingCharacters(in: .whitespacesAndNewlines)
110
- do {
111
- guard let rsaPublicKeyData: Data = Data(base64Encoded: String(pubKey)) else {
19
+ guard let checksumBytes = Data(base64Encoded: checksum) else {
20
+ print("\(CapacitorUpdater.TAG) Cannot decode checksum as base64: \(checksum)")
112
21
  throw CustomError.cannotDecode
113
22
  }
114
23
 
115
- guard let publicKey: SecKey = .loadPublicFromData(rsaPublicKeyData) else {
24
+ if checksumBytes.isEmpty {
25
+ print("\(CapacitorUpdater.TAG) Decoded checksum is empty")
116
26
  throw CustomError.cannotDecode
117
27
  }
118
28
 
119
- return RSAPublicKey(publicKey: publicKey)
120
- } catch {
121
- print("Error load RSA: \(error)")
122
- return nil
123
- }
124
- }
125
-
126
- // code is copied from here: https://github.com/btnguyen2k/swiftutils/blob/88494f4c635b6c6d42ef0fb30a7d666acd38c4fa/SwiftUtils/RSAUtils.swift#L393
127
- public static func decryptWithRSAKey(_ encryptedData: Data, rsaKeyRef: SecKey, padding: SecPadding) -> Data? {
128
- let blockSize = SecKeyGetBlockSize(rsaKeyRef)
129
- let dataSize = encryptedData.count / MemoryLayout<UInt8>.size
130
-
131
- var encryptedDataAsArray = [UInt8](repeating: 0, count: dataSize)
132
- (encryptedData as NSData).getBytes(&encryptedDataAsArray, length: dataSize)
133
-
134
- var decryptedData = [UInt8](repeating: 0, count: 0)
135
- var idx = 0
136
- while idx < encryptedDataAsArray.count {
137
- var idxEnd = idx + blockSize
138
- if idxEnd > encryptedDataAsArray.count {
139
- idxEnd = encryptedDataAsArray.count
140
- }
141
- var chunkData = [UInt8](repeating: 0, count: blockSize)
142
- for i in idx..<idxEnd {
143
- chunkData[i-idx] = encryptedDataAsArray[i]
144
- }
145
-
146
- var decryptedDataBuffer = [UInt8](repeating: 0, count: blockSize)
147
- var decryptedDataLength = blockSize
148
-
149
- let status = SecKeyDecrypt(rsaKeyRef, padding, chunkData, idxEnd-idx, &decryptedDataBuffer, &decryptedDataLength)
150
- if status != noErr {
151
- return nil
152
- }
153
- let finalData = removePadding(decryptedDataBuffer)
154
- decryptedData += finalData
155
-
156
- idx += blockSize
157
- }
158
-
159
- return Data(decryptedData)
160
- }
161
-
162
- // code is copied from here: https://github.com/btnguyen2k/swiftutils/blob/88494f4c635b6c6d42ef0fb30a7d666acd38c4fa/SwiftUtils/RSAUtils.swift#L429
163
- private static func removePadding(_ data: [UInt8]) -> [UInt8] {
164
- var idxFirstZero = -1
165
- var idxNextZero = data.count
166
- for i in 0..<data.count {
167
- if data[i] == 0 {
168
- if idxFirstZero < 0 {
169
- idxFirstZero = i
170
- } else {
171
- idxNextZero = i
172
- break
173
- }
174
- }
175
- }
176
- if idxNextZero-idxFirstZero-1 == 0 {
177
- idxNextZero = idxFirstZero
178
- idxFirstZero = -1
179
- }
180
- var newData = [UInt8](repeating: 0, count: idxNextZero-idxFirstZero-1)
181
- for i in idxFirstZero+1..<idxNextZero {
182
- newData[i-idxFirstZero-1] = data[i]
183
- }
184
- return newData
185
- }
186
- }
187
-
188
- fileprivate extension SecKey {
189
- func exportToData() -> Data? {
190
- var error: Unmanaged<CFError>?
191
- if let cfData: CFData = SecKeyCopyExternalRepresentation(self, &error) {
192
- if error != nil {
193
- return nil
194
- } else {
195
- return cfData as Data
196
- }
197
- } else {
198
- return nil
199
- }
200
- }
201
- static func loadPublicFromData(_ data: Data) -> SecKey? {
202
- let keyDict: [NSObject: NSObject] = [
203
- kSecAttrKeyType: kSecAttrKeyTypeRSA,
204
- kSecAttrKeyClass: kSecAttrKeyClassPublic,
205
- kSecAttrKeySizeInBits: CryptoCipherConstants.rsaKeySizeInBits
206
- ]
207
- return SecKeyCreateWithData(data as NSData, keyDict as CFDictionary, nil)
208
- }
209
- static func loadPrivateFromData(_ data: Data) -> SecKey? {
210
- let keyDict: [NSObject: NSObject] = [
211
- kSecAttrKeyType: kSecAttrKeyTypeRSA,
212
- kSecAttrKeyClass: kSecAttrKeyClassPrivate,
213
- kSecAttrKeySizeInBits: CryptoCipherConstants.rsaKeySizeInBits
214
- ]
215
- return SecKeyCreateWithData(data as CFData, keyDict as CFDictionary, nil)
216
- }
217
- }
218
-
219
- public struct CryptoCipherV2 {
220
-
221
- public static func decryptChecksum(checksum: String, publicKey: String, version: String) throws -> String {
222
- if publicKey.isEmpty {
223
- return checksum
224
- }
225
- do {
226
- let checksumBytes: Data = Data(base64Encoded: checksum)!
227
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: publicKey) else {
228
- print("cannot decode publicKey", publicKey)
29
+ guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
30
+ print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
229
31
  throw CustomError.cannotDecode
230
32
  }
33
+
231
34
  guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
35
+ print("\(CapacitorUpdater.TAG) decryptChecksum fail")
232
36
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
233
37
  }
38
+
234
39
  return decryptedChecksum.base64EncodedString()
235
40
  } catch {
236
- print("\(CapacitorUpdater.TAG) Cannot decrypt checksum: \(checksum)", error)
41
+ print("\(CapacitorUpdater.TAG) decryptChecksum fail: \(error.localizedDescription)")
237
42
  throw CustomError.cannotDecode
238
43
  }
239
44
  }
@@ -242,13 +47,35 @@ public struct CryptoCipherV2 {
242
47
  var sha256 = SHA256()
243
48
 
244
49
  do {
245
- let fileHandle = try FileHandle(forReadingFrom: filePath)
50
+ let fileHandle: FileHandle
51
+ do {
52
+ fileHandle = try FileHandle(forReadingFrom: filePath)
53
+ } catch {
54
+ print("\(CapacitorUpdater.TAG) Cannot open file for checksum: \(filePath.path)", error)
55
+ return ""
56
+ }
57
+
246
58
  defer {
247
- fileHandle.closeFile()
59
+ do {
60
+ try fileHandle.close()
61
+ } catch {
62
+ print("\(CapacitorUpdater.TAG) Error closing file: \(error)")
63
+ }
248
64
  }
249
65
 
250
66
  while autoreleasepool(invoking: {
251
- let fileData = fileHandle.readData(ofLength: bufferSize)
67
+ let fileData: Data
68
+ do {
69
+ if #available(iOS 13.4, *) {
70
+ fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
71
+ } else {
72
+ fileData = fileHandle.readData(ofLength: bufferSize)
73
+ }
74
+ } catch {
75
+ print("\(CapacitorUpdater.TAG) Error reading file: \(error)")
76
+ return false
77
+ }
78
+
252
79
  if fileData.count > 0 {
253
80
  sha256.update(data: fileData)
254
81
  return true // Continue
@@ -266,44 +93,89 @@ public struct CryptoCipherV2 {
266
93
  }
267
94
 
268
95
  public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
269
- if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
270
- print("\(CapacitorUpdater.TAG) Cannot find public key or sessionKey")
96
+ if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
97
+ print("\(CapacitorUpdater.TAG) Cannot found public key or sessionKey")
271
98
  return
272
99
  }
100
+
101
+ if !publicKey.hasPrefix("-----BEGIN RSA PUBLIC KEY-----") {
102
+ print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
103
+ return
104
+ }
105
+
273
106
  do {
274
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: publicKey) else {
275
- print("cannot decode publicKey", publicKey)
107
+ guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
108
+ print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
276
109
  throw CustomError.cannotDecode
277
110
  }
278
111
 
279
- let sessionKeyArray: [String] = sessionKey.components(separatedBy: ":")
280
- guard let ivData: Data = Data(base64Encoded: sessionKeyArray[0]) else {
281
- print("cannot decode sessionKey", sessionKey)
112
+ let sessionKeyComponents = sessionKey.components(separatedBy: ":")
113
+ let ivBase64 = sessionKeyComponents[0]
114
+ let encryptedKeyBase64 = sessionKeyComponents[1]
115
+
116
+ guard let ivData = Data(base64Encoded: ivBase64) else {
117
+ print("\(CapacitorUpdater.TAG) Cannot decode sessionKey IV", ivBase64)
282
118
  throw CustomError.cannotDecode
283
119
  }
284
120
 
285
- guard let sessionKeyDataEncrypted = Data(base64Encoded: sessionKeyArray[1]) else {
121
+ if ivData.count != 16 {
122
+ print("\(CapacitorUpdater.TAG) IV data has invalid length: \(ivData.count), expected 16")
123
+ throw CustomError.cannotDecode
124
+ }
125
+
126
+ guard let sessionKeyDataEncrypted = Data(base64Encoded: encryptedKeyBase64) else {
127
+ print("\(CapacitorUpdater.TAG) Cannot decode sessionKey data", encryptedKeyBase64)
286
128
  throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
287
129
  }
288
130
 
289
131
  guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
132
+ print("\(CapacitorUpdater.TAG) Failed to decrypt session key data")
290
133
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
291
134
  }
292
135
 
136
+ if sessionKeyDataDecrypted.count != 16 {
137
+ print("\(CapacitorUpdater.TAG) Decrypted session key has invalid length: \(sessionKeyDataDecrypted.count), expected 16")
138
+ throw NSError(domain: "Invalid decrypted session key", code: 5, userInfo: nil)
139
+ }
140
+
293
141
  let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
294
142
 
295
- guard let encryptedData = try? Data(contentsOf: filePath) else {
143
+ let encryptedData: Data
144
+ do {
145
+ encryptedData = try Data(contentsOf: filePath)
146
+ } catch {
147
+ print("\(CapacitorUpdater.TAG) Failed to read encrypted data: \(error)")
296
148
  throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
297
149
  }
298
150
 
151
+ if encryptedData.isEmpty {
152
+ print("\(CapacitorUpdater.TAG) Encrypted file data is empty")
153
+ throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
154
+ }
155
+
299
156
  guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
157
+ print("\(CapacitorUpdater.TAG) Failed to decrypt data")
300
158
  throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
301
159
  }
302
160
 
303
- try decryptedData.write(to: filePath)
161
+ if decryptedData.isEmpty {
162
+ print("\(CapacitorUpdater.TAG) Decrypted data is empty")
163
+ throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
164
+ }
165
+
166
+ do {
167
+ try decryptedData.write(to: filePath, options: .atomic)
168
+ if !FileManager.default.fileExists(atPath: filePath.path) {
169
+ print("\(CapacitorUpdater.TAG) File was not created after write")
170
+ throw NSError(domain: "File write failed", code: 8, userInfo: nil)
171
+ }
172
+ } catch {
173
+ print("\(CapacitorUpdater.TAG) Error writing decrypted file: \(error)")
174
+ throw error
175
+ }
304
176
 
305
177
  } catch {
306
- print("\(CapacitorUpdater.TAG) Cannot decode: \(filePath.path)", error)
178
+ print("\(CapacitorUpdater.TAG) decryptFile fail")
307
179
  throw CustomError.cannotDecode
308
180
  }
309
181
  }
@@ -0,0 +1,273 @@
1
+ import BigInt
2
+ import CommonCrypto
3
+ import CryptoKit
4
+
5
+ ///
6
+ /// Constants
7
+ ///
8
+ private enum RSAConstants {
9
+ static let rsaKeySizeInBits: NSNumber = 2048
10
+ static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
11
+ }
12
+
13
+ // We do all this stuff because ios is shit and open source libraries allow to do decryption with public key
14
+ // So we have to do it manually, while in nodejs or Java it's ok and done at language level.
15
+
16
+ ///
17
+ /// The RSA public key.
18
+ ///
19
+ public struct RSAPublicKey {
20
+ private let manualKey: ManualRSAPublicKey
21
+
22
+ fileprivate init(manualKey: ManualRSAPublicKey) {
23
+ self.manualKey = manualKey
24
+ }
25
+
26
+ ///
27
+ /// Takes the data and uses the public key to decrypt it.
28
+ /// Returns the decrypted data.
29
+ ///
30
+ public func decrypt(data: Data) -> Data? {
31
+ return manualKey.decrypt(data)
32
+ }
33
+
34
+ ///
35
+ /// Allows you to load an RSA public key (i.e. one downloaded from the net).
36
+ ///
37
+ public static func load(rsaPublicKey: String) -> RSAPublicKey? {
38
+ // Clean up the key string
39
+ var pubKey: String = rsaPublicKey
40
+ pubKey = pubKey.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
41
+ pubKey = pubKey.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
42
+ pubKey = pubKey.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
43
+ pubKey = pubKey.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
44
+ pubKey = pubKey.replacingOccurrences(of: "\\n+", with: "", options: .regularExpression)
45
+ pubKey = pubKey.replacingOccurrences(of: "\n", with: "")
46
+ pubKey = pubKey.trimmingCharacters(in: .whitespacesAndNewlines)
47
+
48
+ do {
49
+ guard let rsaPublicKeyData: Data = Data(base64Encoded: String(pubKey)) else {
50
+ throw CustomError.cannotDecode
51
+ }
52
+
53
+ // Try parsing as PKCS#1
54
+ if let manualKey = ManualRSAPublicKey.fromPKCS1(rsaPublicKeyData) {
55
+ return RSAPublicKey(manualKey: manualKey)
56
+ }
57
+
58
+ // Most common public exponent is 65537 (0x010001)
59
+ let commonExponent = Data([0x01, 0x00, 0x01]) // 65537 in big-endian
60
+
61
+ // Assume the entire key data is the modulus
62
+ let lastResortKey = ManualRSAPublicKey(modulus: rsaPublicKeyData, exponent: commonExponent)
63
+ return RSAPublicKey(manualKey: lastResortKey)
64
+ } catch {
65
+ return nil
66
+ }
67
+ }
68
+ }
69
+
70
+ // Manual RSA Public Key Implementation using the BigInt library
71
+ struct ManualRSAPublicKey {
72
+ let modulus: BigInt
73
+ let exponent: BigInt
74
+
75
+ init(modulus: Data, exponent: Data) {
76
+ // Create positive BigInts from Data
77
+ let modulusBytes = [UInt8](modulus)
78
+ var modulusValue = BigUInt(0)
79
+ for byte in modulusBytes {
80
+ modulusValue = (modulusValue << 8) | BigUInt(byte)
81
+ }
82
+ self.modulus = BigInt(modulusValue)
83
+ let exponentBytes = [UInt8](exponent)
84
+ var exponentValue = BigUInt(0)
85
+ for byte in exponentBytes {
86
+ exponentValue = (exponentValue << 8) | BigUInt(byte)
87
+ }
88
+ self.exponent = BigInt(exponentValue)
89
+ }
90
+
91
+ // Parse PKCS#1 format public key
92
+ static func fromPKCS1(_ publicKeyData: Data) -> ManualRSAPublicKey? {
93
+ // Parse ASN.1 DER encoded RSA public key
94
+ // Format: RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
95
+
96
+ guard publicKeyData.count > 0 else {
97
+ return nil
98
+ }
99
+
100
+ let bytes = [UInt8](publicKeyData)
101
+
102
+ // Check for sequence tag (0x30)
103
+ guard bytes[0] == 0x30 else {
104
+ // Try direct modulus/exponent approach as fallback
105
+ if publicKeyData.count >= 3 {
106
+ // Assume this is a raw RSA public key with modulus + exponent
107
+ // Most common: modulus is 256 bytes (2048 bits), exponent is 3 bytes (0x010001 = 65537)
108
+ let modulusSize = publicKeyData.count - 3
109
+ if modulusSize > 0 {
110
+ let modulusData = publicKeyData.prefix(modulusSize)
111
+ let exponentData = publicKeyData.suffix(3)
112
+ return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
113
+ }
114
+ }
115
+
116
+ return nil
117
+ }
118
+
119
+ var index = 1
120
+
121
+ // Skip length
122
+ if bytes[index] & 0x80 != 0 {
123
+ let lenBytes = Int(bytes[index] & 0x7F)
124
+ if (index + 1 + lenBytes) >= bytes.count {
125
+ return nil
126
+ }
127
+ index += 1 + lenBytes
128
+ } else {
129
+ index += 1
130
+ }
131
+
132
+ // Check for INTEGER tag for modulus (0x02)
133
+ if index >= bytes.count {
134
+ return nil
135
+ }
136
+
137
+ guard bytes[index] == 0x02 else {
138
+ return nil
139
+ }
140
+ index += 1
141
+
142
+ // Get modulus length
143
+ if index >= bytes.count {
144
+ return nil
145
+ }
146
+
147
+ var modulusLength = 0
148
+ if bytes[index] & 0x80 != 0 {
149
+ let lenBytes = Int(bytes[index] & 0x7F)
150
+ if (index + 1 + lenBytes) >= bytes.count {
151
+ return nil
152
+ }
153
+ index += 1
154
+ for i in 0..<lenBytes {
155
+ modulusLength = (modulusLength << 8) | Int(bytes[index + i])
156
+ }
157
+ index += lenBytes
158
+ } else {
159
+ modulusLength = Int(bytes[index])
160
+ index += 1
161
+ }
162
+
163
+ // Skip any leading zero in modulus (for unsigned integer)
164
+ if index < bytes.count && bytes[index] == 0x00 {
165
+ index += 1
166
+ modulusLength -= 1
167
+ }
168
+
169
+ // Extract modulus
170
+ if (index + modulusLength) > bytes.count {
171
+ return nil
172
+ }
173
+
174
+ let modulusData = Data(bytes[index..<(index + modulusLength)])
175
+ index += modulusLength
176
+
177
+ // Check for INTEGER tag for exponent
178
+ if index >= bytes.count {
179
+ return nil
180
+ }
181
+
182
+ guard bytes[index] == 0x02 else {
183
+ return nil
184
+ }
185
+ index += 1
186
+
187
+ // Get exponent length
188
+ if index >= bytes.count {
189
+ return nil
190
+ }
191
+
192
+ var exponentLength = 0
193
+ if bytes[index] & 0x80 != 0 {
194
+ let lenBytes = Int(bytes[index] & 0x7F)
195
+ if (index + 1 + lenBytes) >= bytes.count {
196
+ return nil
197
+ }
198
+ index += 1
199
+ for i in 0..<lenBytes {
200
+ exponentLength = (exponentLength << 8) | Int(bytes[index + i])
201
+ }
202
+ index += lenBytes
203
+ } else {
204
+ exponentLength = Int(bytes[index])
205
+ index += 1
206
+ }
207
+
208
+ // Extract exponent
209
+ if (index + exponentLength) > bytes.count {
210
+ return nil
211
+ }
212
+
213
+ let exponentData = Data(bytes[index..<(index + exponentLength)])
214
+ return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
215
+ }
216
+
217
+ // Decrypt data using raw RSA operation (c^d mod n)
218
+ func decrypt(_ encryptedData: Data) -> Data? {
219
+ // Create positive BigInt from encrypted data
220
+ let encryptedBytes = [UInt8](encryptedData)
221
+ var encryptedValue = BigUInt(0)
222
+ for byte in encryptedBytes {
223
+ encryptedValue = (encryptedValue << 8) | BigUInt(byte)
224
+ }
225
+ let encrypted = BigInt(encryptedValue)
226
+
227
+ // In Node.js:
228
+ // privateEncrypt uses the private key (d) to encrypt
229
+ // publicDecrypt uses the public key (e) to decrypt
230
+ // The operation we want is: ciphertext^e mod n
231
+
232
+ // RSA operation: c^e mod n
233
+ let decrypted = encrypted.manualPower(exponent, modulus: modulus)
234
+
235
+ // Convert to bytes with proper padding
236
+ guard let bigUIntValue = decrypted.magnitude as? BigUInt else {
237
+ return nil
238
+ }
239
+
240
+ // Convert BigUInt to bytes with padding
241
+ var resultBytes = [UInt8]()
242
+ var tempValue = bigUIntValue
243
+ while tempValue > 0 {
244
+ let byte = UInt8(tempValue & 0xFF)
245
+ resultBytes.insert(byte, at: 0) // Prepend to get big-endian
246
+ tempValue >>= 8
247
+ }
248
+
249
+ // Ensure we have at least 256 bytes (2048 bits) with leading zeros
250
+ let paddedBytes = [UInt8](repeating: 0, count: max(0, 256 - resultBytes.count)) + resultBytes
251
+
252
+ // For PKCS1 padding from Node.js privateEncrypt, the format is:
253
+ // 0x00 || 0x01 || PS || 0x00 || actual data
254
+ // where PS is a string of 0xFF bytes
255
+
256
+ // Check for privateEncrypt padding format (0x00 || 0x01 || PS || 0x00)
257
+ var startIndex = 0
258
+ if paddedBytes.count > 2 && paddedBytes[0] == 0x00 && paddedBytes[1] == 0x01 {
259
+ for i in 2..<paddedBytes.count {
260
+ if paddedBytes[i] == 0x00 {
261
+ startIndex = i + 1
262
+ break
263
+ }
264
+ }
265
+ }
266
+ if startIndex < paddedBytes.count {
267
+ let result = Data(paddedBytes[startIndex...])
268
+ return result
269
+ } else {
270
+ return Data(paddedBytes)
271
+ }
272
+ }
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.0.36",
3
+ "version": "7.0.38",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",