@capgo/capacitor-updater 7.13.16 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CapgoCapacitorUpdater.podspec +1 -0
  2. package/README.md +51 -0
  3. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +30 -1
  4. package/dist/docs.json +107 -0
  5. package/dist/esm/definitions.d.ts +31 -0
  6. package/dist/esm/definitions.js.map +1 -1
  7. package/dist/esm/web.d.ts +6 -0
  8. package/dist/esm/web.js +8 -0
  9. package/dist/esm/web.js.map +1 -1
  10. package/dist/plugin.cjs.js +8 -0
  11. package/dist/plugin.cjs.js.map +1 -1
  12. package/dist/plugin.js +8 -0
  13. package/dist/plugin.js.map +1 -1
  14. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  15. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  16. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +113 -0
  17. package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +48 -0
  18. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1203 -0
  19. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1301 -0
  20. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +187 -0
  21. package/ios/Sources/CapacitorUpdaterPlugin/DelayCondition.swift +74 -0
  22. package/ios/Sources/CapacitorUpdaterPlugin/DelayUntilNext.swift +30 -0
  23. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +222 -0
  24. package/ios/Sources/CapacitorUpdaterPlugin/Info.plist +28 -0
  25. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +303 -0
  26. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  27. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  28. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  29. package/ios/Sources/CapacitorUpdaterPlugin/UserDefaultsExtension.swift +46 -0
  30. package/package.json +2 -2
@@ -0,0 +1,187 @@
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
+ */
6
+
7
+ import Foundation
8
+ import CryptoKit
9
+ import BigInt
10
+
11
+ public struct CryptoCipher {
12
+ private static var logger: Logger!
13
+
14
+ public static func setLogger(_ logger: Logger) {
15
+ self.logger = logger
16
+ }
17
+
18
+ public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
19
+ if publicKey.isEmpty {
20
+ logger.info("No encryption set (public key) ignored")
21
+ return checksum
22
+ }
23
+ do {
24
+ guard let checksumBytes = Data(base64Encoded: checksum) else {
25
+ logger.error("Cannot decode checksum as base64: \(checksum)")
26
+ throw CustomError.cannotDecode
27
+ }
28
+
29
+ if checksumBytes.isEmpty {
30
+ logger.error("Decoded checksum is empty")
31
+ throw CustomError.cannotDecode
32
+ }
33
+
34
+ guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
35
+ logger.error("The public key is not a valid RSA Public key")
36
+ throw CustomError.cannotDecode
37
+ }
38
+
39
+ guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
40
+ logger.error("decryptChecksum fail")
41
+ throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
42
+ }
43
+
44
+ return decryptedChecksum.base64EncodedString()
45
+ } catch {
46
+ logger.error("decryptChecksum fail: \(error.localizedDescription)")
47
+ throw CustomError.cannotDecode
48
+ }
49
+ }
50
+ public static func calcChecksum(filePath: URL) -> String {
51
+ let bufferSize = 1024 * 1024 * 5 // 5 MB
52
+ var sha256 = SHA256()
53
+
54
+ do {
55
+ let fileHandle: FileHandle
56
+ do {
57
+ fileHandle = try FileHandle(forReadingFrom: filePath)
58
+ } catch {
59
+ logger.error("Cannot open file for checksum: \(filePath.path) \(error)")
60
+ return ""
61
+ }
62
+
63
+ defer {
64
+ do {
65
+ try fileHandle.close()
66
+ } catch {
67
+ logger.error("Error closing file: \(error)")
68
+ }
69
+ }
70
+
71
+ while autoreleasepool(invoking: {
72
+ let fileData: Data
73
+ do {
74
+ if #available(iOS 13.4, *) {
75
+ fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
76
+ } else {
77
+ fileData = fileHandle.readData(ofLength: bufferSize)
78
+ }
79
+ } catch {
80
+ logger.error("Error reading file: \(error)")
81
+ return false
82
+ }
83
+
84
+ if fileData.count > 0 {
85
+ sha256.update(data: fileData)
86
+ return true // Continue
87
+ } else {
88
+ return false // End of file
89
+ }
90
+ }) {}
91
+
92
+ let digest = sha256.finalize()
93
+ return digest.compactMap { String(format: "%02x", $0) }.joined()
94
+ } catch {
95
+ logger.error("Cannot get checksum: \(filePath.path) \(error)")
96
+ return ""
97
+ }
98
+ }
99
+
100
+ public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
101
+ if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
102
+ logger.info("Encryption not set, no public key or seesion, ignored")
103
+ return
104
+ }
105
+
106
+ if !publicKey.hasPrefix("-----BEGIN RSA PUBLIC KEY-----") {
107
+ logger.error("The public key is not a valid RSA Public key")
108
+ return
109
+ }
110
+
111
+ do {
112
+ guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
113
+ logger.error("The public key is not a valid RSA Public key")
114
+ throw CustomError.cannotDecode
115
+ }
116
+
117
+ let sessionKeyComponents = sessionKey.components(separatedBy: ":")
118
+ let ivBase64 = sessionKeyComponents[0]
119
+ let encryptedKeyBase64 = sessionKeyComponents[1]
120
+
121
+ guard let ivData = Data(base64Encoded: ivBase64) else {
122
+ logger.error("Cannot decode sessionKey IV \(ivBase64)")
123
+ throw CustomError.cannotDecode
124
+ }
125
+
126
+ if ivData.count != 16 {
127
+ logger.error("IV data has invalid length: \(ivData.count), expected 16")
128
+ throw CustomError.cannotDecode
129
+ }
130
+
131
+ guard let sessionKeyDataEncrypted = Data(base64Encoded: encryptedKeyBase64) else {
132
+ logger.error("Cannot decode sessionKey data \(encryptedKeyBase64)")
133
+ throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
134
+ }
135
+
136
+ guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
137
+ logger.error("Failed to decrypt session key data")
138
+ throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
139
+ }
140
+
141
+ if sessionKeyDataDecrypted.count != 16 {
142
+ logger.error("Decrypted session key has invalid length: \(sessionKeyDataDecrypted.count), expected 16")
143
+ throw NSError(domain: "Invalid decrypted session key", code: 5, userInfo: nil)
144
+ }
145
+
146
+ let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger)
147
+
148
+ let encryptedData: Data
149
+ do {
150
+ encryptedData = try Data(contentsOf: filePath)
151
+ } catch {
152
+ logger.error("Failed to read encrypted data: \(error)")
153
+ throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
154
+ }
155
+
156
+ if encryptedData.isEmpty {
157
+ logger.error("Encrypted file data is empty")
158
+ throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
159
+ }
160
+
161
+ guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
162
+ logger.error("Failed to decrypt data")
163
+ throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
164
+ }
165
+
166
+ if decryptedData.isEmpty {
167
+ logger.error("Decrypted data is empty")
168
+ throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
169
+ }
170
+
171
+ do {
172
+ try decryptedData.write(to: filePath, options: .atomic)
173
+ if !FileManager.default.fileExists(atPath: filePath.path) {
174
+ logger.error("File was not created after write")
175
+ throw NSError(domain: "File write failed", code: 8, userInfo: nil)
176
+ }
177
+ } catch {
178
+ logger.error("Error writing decrypted file: \(error)")
179
+ throw error
180
+ }
181
+
182
+ } catch {
183
+ logger.error("decryptFile fail")
184
+ throw CustomError.cannotDecode
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,74 @@
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
+ */
6
+
7
+ //
8
+ // DelayCondition.swift
9
+ // Plugin
10
+ //
11
+ // Created by Luca Peruzzo on 12/09/22.
12
+ // Copyright © 2022 Capgo. All rights reserved.
13
+ //
14
+
15
+ import Foundation
16
+
17
+ private func delayUntilNextValue(value: String) -> DelayUntilNext {
18
+ switch value {
19
+ case "background": return .background
20
+ case "kill": return .kill
21
+ case "nativeVersion": return .nativeVersion
22
+ case "date": return .date
23
+ default:
24
+ return .background
25
+ }
26
+ }
27
+
28
+ @objc public class DelayCondition: NSObject, Decodable, Encodable {
29
+ private let kind: DelayUntilNext
30
+ private let value: String?
31
+
32
+ convenience init(kind: String, value: String?) {
33
+ self.init(kind: delayUntilNextValue(value: kind), value: value)
34
+ }
35
+
36
+ init(kind: DelayUntilNext, value: String?) {
37
+ self.kind = kind
38
+ self.value = value
39
+ }
40
+
41
+ public required init(from decoder: Decoder) throws {
42
+ let values: KeyedDecodingContainer<DelayCondition.CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
43
+ kind = try values.decode(DelayUntilNext.self, forKey: .kind)
44
+ value = try values.decode(String.self, forKey: .value)
45
+ }
46
+
47
+ enum CodingKeys: String, CodingKey {
48
+ case kind, value
49
+ }
50
+
51
+ public func getKind() -> String {
52
+ return self.kind.description
53
+ }
54
+
55
+ public func getValue() -> String? {
56
+ return self.value
57
+ }
58
+
59
+ public func toJSON() -> [String: String] {
60
+ return [
61
+ "kind": self.getKind(),
62
+ "value": self.getValue() ?? ""
63
+ ]
64
+ }
65
+
66
+ public static func == (lhs: DelayCondition, rhs: DelayCondition) -> Bool {
67
+ return lhs.getKind() == rhs.getKind() && lhs.getValue() == rhs.getValue()
68
+ }
69
+
70
+ public func toString() -> String {
71
+ return "{ \"kind\": \"\(self.getKind())\", \"value\": \"\(self.getValue() ?? "")\"}"
72
+ }
73
+
74
+ }
@@ -0,0 +1,30 @@
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
+ */
6
+
7
+ //
8
+ // DelayUntilNext.swift
9
+ // Plugin
10
+ //
11
+ // Created by Luca Peruzzo on 12/09/22.
12
+ // Copyright © 2022 Capgo. All rights reserved.
13
+ //
14
+
15
+ import Foundation
16
+ enum DelayUntilNext: Decodable, Encodable, CustomStringConvertible {
17
+ case background
18
+ case kill
19
+ case nativeVersion
20
+ case date
21
+
22
+ var description: String {
23
+ switch self {
24
+ case .background: return "background"
25
+ case .kill: return "kill"
26
+ case .nativeVersion: return "nativeVersion"
27
+ case .date: return "date"
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,222 @@
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
+ */
6
+
7
+ //
8
+ // DelayUpdateUtils.swift
9
+ // Plugin
10
+ //
11
+ // Created by Auto-generated based on Android implementation
12
+ // Copyright © 2024 Capgo. All rights reserved.
13
+ //
14
+
15
+ import Foundation
16
+ import Version
17
+
18
+ public class DelayUpdateUtils {
19
+
20
+ static let DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO"
21
+ static let BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO"
22
+ private let logger: Logger
23
+
24
+ private let currentVersionNative: Version
25
+ private let installNext: () -> Void
26
+
27
+ public enum CancelDelaySource {
28
+ case killed
29
+ case background
30
+ case foreground
31
+
32
+ var description: String {
33
+ switch self {
34
+ case .killed: return "KILLED"
35
+ case .background: return "BACKGROUND"
36
+ case .foreground: return "FOREGROUND"
37
+ }
38
+ }
39
+ }
40
+
41
+ public init(currentVersionNative: Version, installNext: @escaping () -> Void, logger: Logger) {
42
+ self.currentVersionNative = currentVersionNative
43
+ self.installNext = installNext
44
+ self.logger = logger
45
+ }
46
+
47
+ public func checkCancelDelay(source: CancelDelaySource) {
48
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
49
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
50
+ let kind: String = obj.value(forKey: "kind") as! String
51
+ let value: String? = obj.value(forKey: "value") as? String
52
+ return DelayCondition(kind: kind, value: value)
53
+ }
54
+
55
+ var delayConditionListToKeep: [DelayCondition] = []
56
+ var index = 0
57
+
58
+ for condition in delayConditionList {
59
+ let kind = condition.getKind()
60
+ let value = condition.getValue()
61
+
62
+ switch kind {
63
+ case "background":
64
+ if source == .foreground {
65
+ let backgroundedAt = getBackgroundTimestamp()
66
+ let now = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
67
+ let delta = max(0, now - backgroundedAt)
68
+
69
+ var longValue: Int64 = 0
70
+ if let value = value, !value.isEmpty {
71
+ longValue = Int64(value) ?? 0
72
+ }
73
+
74
+ if delta > longValue {
75
+ logger.info("Background condition (value: \(value ?? "")) deleted at index \(index). Delta: \(delta), longValue: \(longValue)")
76
+ } else {
77
+ delayConditionListToKeep.append(condition)
78
+ logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
79
+ }
80
+ } else {
81
+ delayConditionListToKeep.append(condition)
82
+ logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
83
+ }
84
+
85
+ case "kill":
86
+ if source == .killed {
87
+ self.installNext()
88
+ } else {
89
+ delayConditionListToKeep.append(condition)
90
+ logger.info("Kill delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
91
+ }
92
+
93
+ case "date":
94
+ if let value = value, !value.isEmpty {
95
+ do {
96
+ let dateFormatter = ISO8601DateFormatter()
97
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
98
+
99
+ if let date = dateFormatter.date(from: value) {
100
+ if Date() > date {
101
+ logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
102
+ } else {
103
+ delayConditionListToKeep.append(condition)
104
+ logger.info("Date delay (value: \(value)) condition kept at index \(index)")
105
+ }
106
+ } else {
107
+ logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
108
+ }
109
+ } catch {
110
+ logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
111
+ }
112
+ } else {
113
+ logger.error("Date delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
114
+ }
115
+
116
+ case "nativeVersion":
117
+ if let value = value, !value.isEmpty {
118
+ do {
119
+ let versionLimit = try Version(value)
120
+ if currentVersionNative >= versionLimit {
121
+ logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
122
+ } else {
123
+ delayConditionListToKeep.append(condition)
124
+ logger.info("Native version delay (value: \(value)) condition kept at index \(index)")
125
+ }
126
+ } catch {
127
+ logger.error("Native version delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
128
+ }
129
+ } else {
130
+ logger.error("Native version delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
131
+ }
132
+
133
+ default:
134
+ logger.error("Unknown delay condition kind: \(kind) at index \(index)")
135
+ }
136
+
137
+ index += 1
138
+ }
139
+
140
+ if !delayConditionListToKeep.isEmpty {
141
+ let json = toJson(object: delayConditionListToKeep.map { $0.toJSON() })
142
+ _ = setMultiDelay(delayConditions: json)
143
+ } else {
144
+ // Clear all delay conditions if none are left to keep
145
+ _ = cancelDelay(source: "checkCancelDelay")
146
+ }
147
+ }
148
+
149
+ public func setMultiDelay(delayConditions: String) -> Bool {
150
+ do {
151
+ UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
152
+ UserDefaults.standard.synchronize()
153
+ logger.info("Delay update saved")
154
+ return true
155
+ } catch {
156
+ logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
157
+ return false
158
+ }
159
+ }
160
+
161
+ public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
162
+ do {
163
+ UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
164
+ UserDefaults.standard.synchronize()
165
+ logger.info("Background timestamp saved")
166
+ } catch {
167
+ logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
168
+ }
169
+ }
170
+
171
+ public func unsetBackgroundTimestamp() {
172
+ do {
173
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
174
+ UserDefaults.standard.synchronize()
175
+ logger.info("Background timestamp removed")
176
+ } catch {
177
+ logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
178
+ }
179
+ }
180
+
181
+ private func getBackgroundTimestamp() -> Int64 {
182
+ do {
183
+ let timestamp = UserDefaults.standard.object(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY) as? Int64 ?? 0
184
+ return timestamp
185
+ } catch {
186
+ logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
187
+ return 0
188
+ }
189
+ }
190
+
191
+ public func cancelDelay(source: String) -> Bool {
192
+ do {
193
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
194
+ UserDefaults.standard.synchronize()
195
+ logger.info("All delays canceled from \(source)")
196
+ return true
197
+ } catch {
198
+ logger.error("Failed to cancel update delay: \(error)")
199
+ return false
200
+ }
201
+ }
202
+
203
+ // MARK: - Helper methods
204
+
205
+ private func toJson(object: Any) -> String {
206
+ guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
207
+ return ""
208
+ }
209
+ return String(data: data, encoding: String.Encoding.utf8) ?? ""
210
+ }
211
+
212
+ private func fromJsonArr(json: String) -> [NSObject] {
213
+ guard let jsonData = json.data(using: .utf8) else {
214
+ return []
215
+ }
216
+ let object = try? JSONSerialization.jsonObject(
217
+ with: jsonData,
218
+ options: .mutableContainers
219
+ ) as? [NSObject]
220
+ return object ?? []
221
+ }
222
+ }
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>NSPrincipalClass</key>
22
+ <string></string>
23
+ <key>UIFileSharingEnabled</key>
24
+ <true/>
25
+ <key>LSSupportsOpeningDocumentsInPlace</key>
26
+ <true/>
27
+ </dict>
28
+ </plist>