@capgo/capacitor-updater 6.14.26 → 6.14.33
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 +3 -2
- package/Package.swift +2 -2
- package/README.md +350 -74
- package/android/build.gradle +20 -8
- package/android/proguard-rules.pro +22 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1202 -510
- package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +566 -154
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
- package/dist/docs.json +652 -63
- package/dist/esm/definitions.d.ts +274 -15
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +29 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +311 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +311 -2
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1578 -0
- package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +408 -139
- package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
- package/package.json +20 -16
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
- /package/{LICENCE → LICENSE} +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift}
RENAMED
|
@@ -161,7 +161,14 @@ fileprivate extension SecKey {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// V1 Encryption - uses privateKey (deprecated but kept for backwards compatibility)
|
|
165
|
+
public struct CryptoCipherV1 {
|
|
166
|
+
private static var logger: Logger?
|
|
167
|
+
|
|
168
|
+
public static func setLogger(_ newLogger: Logger) {
|
|
169
|
+
logger = newLogger
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
public static func calcChecksum(filePath: URL) -> String {
|
|
166
173
|
let bufferSize = 1024 * 1024 * 5 // 5 MB
|
|
167
174
|
var checksum = uLong(0)
|
|
@@ -186,16 +193,16 @@ public struct CryptoCipher {
|
|
|
186
193
|
|
|
187
194
|
return String(format: "%08X", checksum).lowercased()
|
|
188
195
|
} catch {
|
|
189
|
-
print("\(
|
|
196
|
+
print("\("[Capacitor-updater]") Cannot get checksum: \(filePath.path)", error)
|
|
190
197
|
return ""
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
200
|
public static func decryptFile(filePath: URL, privateKey: String, sessionKey: String, version: String) throws {
|
|
194
201
|
if privateKey.isEmpty {
|
|
195
|
-
print("\(
|
|
202
|
+
print("\("[Capacitor-updater]") Cannot found privateKey")
|
|
196
203
|
return
|
|
197
204
|
} else if sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
|
|
198
|
-
print("\(
|
|
205
|
+
print("\("[Capacitor-updater]") Cannot found sessionKey")
|
|
199
206
|
return
|
|
200
207
|
}
|
|
201
208
|
do {
|
|
@@ -218,7 +225,7 @@ public struct CryptoCipher {
|
|
|
218
225
|
throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
|
|
228
|
+
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger ?? Logger(withTag: "[Capacitor-updater]"))
|
|
222
229
|
|
|
223
230
|
guard let encryptedData = try? Data(contentsOf: filePath) else {
|
|
224
231
|
throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
|
|
@@ -231,7 +238,7 @@ public struct CryptoCipher {
|
|
|
231
238
|
try decryptedData.write(to: filePath)
|
|
232
239
|
|
|
233
240
|
} catch {
|
|
234
|
-
print("\(
|
|
241
|
+
print("\("[Capacitor-updater]") Cannot decode: \(filePath.path)", error)
|
|
235
242
|
throw CustomError.cannotDecode
|
|
236
243
|
}
|
|
237
244
|
}
|
|
@@ -8,37 +8,43 @@ import Foundation
|
|
|
8
8
|
import CryptoKit
|
|
9
9
|
import BigInt
|
|
10
10
|
|
|
11
|
+
// V2 Encryption - uses publicKey (modern encryption from main branch)
|
|
11
12
|
public struct CryptoCipherV2 {
|
|
13
|
+
private static var logger: Logger!
|
|
12
14
|
|
|
13
|
-
public static func
|
|
15
|
+
public static func setLogger(_ logger: Logger) {
|
|
16
|
+
self.logger = logger
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
|
|
14
20
|
if publicKey.isEmpty {
|
|
15
|
-
|
|
21
|
+
logger.info("No encryption set (public key) ignored")
|
|
16
22
|
return checksum
|
|
17
23
|
}
|
|
18
24
|
do {
|
|
19
25
|
guard let checksumBytes = Data(base64Encoded: checksum) else {
|
|
20
|
-
|
|
26
|
+
logger.error("Cannot decode checksum as base64: \(checksum)")
|
|
21
27
|
throw CustomError.cannotDecode
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
if checksumBytes.isEmpty {
|
|
25
|
-
|
|
31
|
+
logger.error("Decoded checksum is empty")
|
|
26
32
|
throw CustomError.cannotDecode
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
|
|
30
|
-
|
|
36
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
31
37
|
throw CustomError.cannotDecode
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
|
|
35
|
-
|
|
41
|
+
logger.error("decryptChecksum fail")
|
|
36
42
|
throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
return decryptedChecksum.base64EncodedString()
|
|
40
46
|
} catch {
|
|
41
|
-
|
|
47
|
+
logger.error("decryptChecksum fail: \(error.localizedDescription)")
|
|
42
48
|
throw CustomError.cannotDecode
|
|
43
49
|
}
|
|
44
50
|
}
|
|
@@ -51,7 +57,7 @@ public struct CryptoCipherV2 {
|
|
|
51
57
|
do {
|
|
52
58
|
fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
53
59
|
} catch {
|
|
54
|
-
|
|
60
|
+
logger.error("Cannot open file for checksum: \(filePath.path) \(error)")
|
|
55
61
|
return ""
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -59,7 +65,7 @@ public struct CryptoCipherV2 {
|
|
|
59
65
|
do {
|
|
60
66
|
try fileHandle.close()
|
|
61
67
|
} catch {
|
|
62
|
-
|
|
68
|
+
logger.error("Error closing file: \(error)")
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -72,7 +78,7 @@ public struct CryptoCipherV2 {
|
|
|
72
78
|
fileData = fileHandle.readData(ofLength: bufferSize)
|
|
73
79
|
}
|
|
74
80
|
} catch {
|
|
75
|
-
|
|
81
|
+
logger.error("Error reading file: \(error)")
|
|
76
82
|
return false
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -87,25 +93,25 @@ public struct CryptoCipherV2 {
|
|
|
87
93
|
let digest = sha256.finalize()
|
|
88
94
|
return digest.compactMap { String(format: "%02x", $0) }.joined()
|
|
89
95
|
} catch {
|
|
90
|
-
|
|
96
|
+
logger.error("Cannot get checksum: \(filePath.path) \(error)")
|
|
91
97
|
return ""
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
|
|
96
102
|
if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
|
|
97
|
-
|
|
103
|
+
logger.info("Encryption not set, no public key or seesion, ignored")
|
|
98
104
|
return
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
if !publicKey.hasPrefix("-----BEGIN RSA PUBLIC KEY-----") {
|
|
102
|
-
|
|
108
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
103
109
|
return
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
do {
|
|
107
113
|
guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
|
|
108
|
-
|
|
114
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
109
115
|
throw CustomError.cannotDecode
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -114,68 +120,68 @@ public struct CryptoCipherV2 {
|
|
|
114
120
|
let encryptedKeyBase64 = sessionKeyComponents[1]
|
|
115
121
|
|
|
116
122
|
guard let ivData = Data(base64Encoded: ivBase64) else {
|
|
117
|
-
|
|
123
|
+
logger.error("Cannot decode sessionKey IV \(ivBase64)")
|
|
118
124
|
throw CustomError.cannotDecode
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
if ivData.count != 16 {
|
|
122
|
-
|
|
128
|
+
logger.error("IV data has invalid length: \(ivData.count), expected 16")
|
|
123
129
|
throw CustomError.cannotDecode
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
guard let sessionKeyDataEncrypted = Data(base64Encoded: encryptedKeyBase64) else {
|
|
127
|
-
|
|
133
|
+
logger.error("Cannot decode sessionKey data \(encryptedKeyBase64)")
|
|
128
134
|
throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
|
|
132
|
-
|
|
138
|
+
logger.error("Failed to decrypt session key data")
|
|
133
139
|
throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
if sessionKeyDataDecrypted.count != 16 {
|
|
137
|
-
|
|
143
|
+
logger.error("Decrypted session key has invalid length: \(sessionKeyDataDecrypted.count), expected 16")
|
|
138
144
|
throw NSError(domain: "Invalid decrypted session key", code: 5, userInfo: nil)
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
|
|
147
|
+
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger)
|
|
142
148
|
|
|
143
149
|
let encryptedData: Data
|
|
144
150
|
do {
|
|
145
151
|
encryptedData = try Data(contentsOf: filePath)
|
|
146
152
|
} catch {
|
|
147
|
-
|
|
153
|
+
logger.error("Failed to read encrypted data: \(error)")
|
|
148
154
|
throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
if encryptedData.isEmpty {
|
|
152
|
-
|
|
158
|
+
logger.error("Encrypted file data is empty")
|
|
153
159
|
throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
|
|
157
|
-
|
|
163
|
+
logger.error("Failed to decrypt data")
|
|
158
164
|
throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
if decryptedData.isEmpty {
|
|
162
|
-
|
|
168
|
+
logger.error("Decrypted data is empty")
|
|
163
169
|
throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
do {
|
|
167
173
|
try decryptedData.write(to: filePath, options: .atomic)
|
|
168
174
|
if !FileManager.default.fileExists(atPath: filePath.path) {
|
|
169
|
-
|
|
175
|
+
logger.error("File was not created after write")
|
|
170
176
|
throw NSError(domain: "File write failed", code: 8, userInfo: nil)
|
|
171
177
|
}
|
|
172
178
|
} catch {
|
|
173
|
-
|
|
179
|
+
logger.error("Error writing decrypted file: \(error)")
|
|
174
180
|
throw error
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
} catch {
|
|
178
|
-
|
|
184
|
+
logger.error("decryptFile fail")
|
|
179
185
|
throw CustomError.cannotDecode
|
|
180
186
|
}
|
|
181
187
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
|
|
26
|
+
public enum CancelDelaySource {
|
|
27
|
+
case killed
|
|
28
|
+
case background
|
|
29
|
+
case foreground
|
|
30
|
+
|
|
31
|
+
var description: String {
|
|
32
|
+
switch self {
|
|
33
|
+
case .killed: return "KILLED"
|
|
34
|
+
case .background: return "BACKGROUND"
|
|
35
|
+
case .foreground: return "FOREGROUND"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public init(currentVersionNative: Version, logger: Logger) {
|
|
41
|
+
self.currentVersionNative = currentVersionNative
|
|
42
|
+
self.logger = logger
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public func checkCancelDelay(source: CancelDelaySource) {
|
|
46
|
+
let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
47
|
+
let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
|
|
48
|
+
let kind: String = obj.value(forKey: "kind") as! String
|
|
49
|
+
let value: String? = obj.value(forKey: "value") as? String
|
|
50
|
+
return DelayCondition(kind: kind, value: value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var delayConditionListToKeep: [DelayCondition] = []
|
|
54
|
+
var index = 0
|
|
55
|
+
|
|
56
|
+
for condition in delayConditionList {
|
|
57
|
+
let kind = condition.getKind()
|
|
58
|
+
let value = condition.getValue()
|
|
59
|
+
|
|
60
|
+
switch kind {
|
|
61
|
+
case "background":
|
|
62
|
+
if source == .foreground {
|
|
63
|
+
let backgroundedAt = getBackgroundTimestamp()
|
|
64
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
|
|
65
|
+
let delta = max(0, now - backgroundedAt)
|
|
66
|
+
|
|
67
|
+
var longValue: Int64 = 0
|
|
68
|
+
if let value = value, !value.isEmpty {
|
|
69
|
+
longValue = Int64(value) ?? 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if delta > longValue {
|
|
73
|
+
logger.info("Background condition (value: \(value ?? "")) deleted at index \(index). Delta: \(delta), longValue: \(longValue)")
|
|
74
|
+
} else {
|
|
75
|
+
delayConditionListToKeep.append(condition)
|
|
76
|
+
logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
delayConditionListToKeep.append(condition)
|
|
80
|
+
logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "kill":
|
|
84
|
+
if source == .killed {
|
|
85
|
+
logger.info("Kill delay (value: \(value ?? "")) condition removed at index \(index) after app kill")
|
|
86
|
+
} else {
|
|
87
|
+
delayConditionListToKeep.append(condition)
|
|
88
|
+
logger.info("Kill delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "date":
|
|
92
|
+
if let value = value, !value.isEmpty {
|
|
93
|
+
do {
|
|
94
|
+
let dateFormatter = ISO8601DateFormatter()
|
|
95
|
+
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
96
|
+
|
|
97
|
+
if let date = dateFormatter.date(from: value) {
|
|
98
|
+
if Date() > date {
|
|
99
|
+
logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
|
|
100
|
+
} else {
|
|
101
|
+
delayConditionListToKeep.append(condition)
|
|
102
|
+
logger.info("Date delay (value: \(value)) condition kept at index \(index)")
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
logger.error("Date delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "nativeVersion":
|
|
115
|
+
if let value = value, !value.isEmpty {
|
|
116
|
+
do {
|
|
117
|
+
let versionLimit = try Version(value)
|
|
118
|
+
if currentVersionNative >= versionLimit {
|
|
119
|
+
logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
|
|
120
|
+
} else {
|
|
121
|
+
delayConditionListToKeep.append(condition)
|
|
122
|
+
logger.info("Native version delay (value: \(value)) condition kept at index \(index)")
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
logger.error("Native version delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
logger.error("Native version delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
logger.error("Unknown delay condition kind: \(kind) at index \(index)")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
index += 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if !delayConditionListToKeep.isEmpty {
|
|
139
|
+
let json = toJson(object: delayConditionListToKeep.map { $0.toJSON() })
|
|
140
|
+
_ = setMultiDelay(delayConditions: json)
|
|
141
|
+
} else {
|
|
142
|
+
// Clear all delay conditions if none are left to keep
|
|
143
|
+
_ = cancelDelay(source: "checkCancelDelay")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public func setMultiDelay(delayConditions: String) -> Bool {
|
|
148
|
+
do {
|
|
149
|
+
UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
150
|
+
UserDefaults.standard.synchronize()
|
|
151
|
+
logger.info("Delay update saved")
|
|
152
|
+
return true
|
|
153
|
+
} catch {
|
|
154
|
+
logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
|
|
160
|
+
do {
|
|
161
|
+
UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
162
|
+
UserDefaults.standard.synchronize()
|
|
163
|
+
logger.info("Background timestamp saved")
|
|
164
|
+
} catch {
|
|
165
|
+
logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public func unsetBackgroundTimestamp() {
|
|
170
|
+
do {
|
|
171
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
172
|
+
UserDefaults.standard.synchronize()
|
|
173
|
+
logger.info("Background timestamp removed")
|
|
174
|
+
} catch {
|
|
175
|
+
logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func getBackgroundTimestamp() -> Int64 {
|
|
180
|
+
do {
|
|
181
|
+
let timestamp = UserDefaults.standard.object(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY) as? Int64 ?? 0
|
|
182
|
+
return timestamp
|
|
183
|
+
} catch {
|
|
184
|
+
logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public func cancelDelay(source: String) -> Bool {
|
|
190
|
+
do {
|
|
191
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
192
|
+
UserDefaults.standard.synchronize()
|
|
193
|
+
logger.info("All delays canceled from \(source)")
|
|
194
|
+
return true
|
|
195
|
+
} catch {
|
|
196
|
+
logger.error("Failed to cancel update delay: \(error)")
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: - Helper methods
|
|
202
|
+
|
|
203
|
+
private func toJson(object: Any) -> String {
|
|
204
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
|
|
205
|
+
return ""
|
|
206
|
+
}
|
|
207
|
+
return String(data: data, encoding: String.Encoding.utf8) ?? ""
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private func fromJsonArr(json: String) -> [NSObject] {
|
|
211
|
+
guard let jsonData = json.data(using: .utf8) else {
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
let object = try? JSONSerialization.jsonObject(
|
|
215
|
+
with: jsonData,
|
|
216
|
+
options: .mutableContainers
|
|
217
|
+
) as? [NSObject]
|
|
218
|
+
return object ?? []
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 Security
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper class to manage device ID persistence across app installations.
|
|
12
|
+
* Uses iOS Keychain to persist the device ID.
|
|
13
|
+
*/
|
|
14
|
+
class DeviceIdHelper {
|
|
15
|
+
private static let keychainService = "app.capgo.updater"
|
|
16
|
+
private static let keychainAccount = "deviceId"
|
|
17
|
+
private static let legacyDefaultsKey = "appUUID"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets or creates a device ID that persists across reinstalls.
|
|
21
|
+
*
|
|
22
|
+
* This method:
|
|
23
|
+
* 1. First checks for an existing ID in Keychain (persists across reinstalls)
|
|
24
|
+
* 2. Falls back to UserDefaults (for migration from older versions)
|
|
25
|
+
* 3. Generates a new UUID if neither exists
|
|
26
|
+
* 4. Stores the ID in Keychain for future use
|
|
27
|
+
*
|
|
28
|
+
* @return Device ID as a lowercase UUID string
|
|
29
|
+
*/
|
|
30
|
+
static func getOrCreateDeviceId() -> String {
|
|
31
|
+
// Try to get device ID from Keychain first
|
|
32
|
+
if let keychainDeviceId = getDeviceIdFromKeychain() {
|
|
33
|
+
return keychainDeviceId.lowercased()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Migration: Check UserDefaults for existing device ID
|
|
37
|
+
var deviceId = UserDefaults.standard.string(forKey: legacyDefaultsKey)
|
|
38
|
+
|
|
39
|
+
if deviceId == nil || deviceId!.isEmpty {
|
|
40
|
+
// Generate new device ID if none exists
|
|
41
|
+
deviceId = UUID().uuidString
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure lowercase for consistency
|
|
45
|
+
deviceId = deviceId!.lowercased()
|
|
46
|
+
|
|
47
|
+
// Save to Keychain for persistence across reinstalls
|
|
48
|
+
saveDeviceIdToKeychain(deviceId: deviceId!)
|
|
49
|
+
|
|
50
|
+
return deviceId!
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retrieves the device ID from iOS Keychain.
|
|
55
|
+
*
|
|
56
|
+
* @return Device ID string or nil if not found
|
|
57
|
+
*/
|
|
58
|
+
private static func getDeviceIdFromKeychain() -> String? {
|
|
59
|
+
let query: [String: Any] = [
|
|
60
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
61
|
+
kSecAttrService as String: keychainService,
|
|
62
|
+
kSecAttrAccount as String: keychainAccount,
|
|
63
|
+
kSecReturnData as String: true,
|
|
64
|
+
kSecMatchLimit as String: kSecMatchLimitOne
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
var result: AnyObject?
|
|
68
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
69
|
+
|
|
70
|
+
guard status == errSecSuccess,
|
|
71
|
+
let data = result as? Data,
|
|
72
|
+
let deviceId = String(data: data, encoding: .utf8) else {
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return deviceId
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Saves the device ID to iOS Keychain with appropriate accessibility settings.
|
|
81
|
+
*
|
|
82
|
+
* Uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
83
|
+
* - Data persists across reinstalls
|
|
84
|
+
* - Data is NOT synced to iCloud
|
|
85
|
+
* - Data is accessible after first device unlock
|
|
86
|
+
* - Data stays on this device only (privacy-friendly)
|
|
87
|
+
*
|
|
88
|
+
* @param deviceId The device ID to save
|
|
89
|
+
*/
|
|
90
|
+
private static func saveDeviceIdToKeychain(deviceId: String) {
|
|
91
|
+
guard let data = deviceId.data(using: .utf8) else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Delete any existing entry first
|
|
96
|
+
let deleteQuery: [String: Any] = [
|
|
97
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
98
|
+
kSecAttrService as String: keychainService,
|
|
99
|
+
kSecAttrAccount as String: keychainAccount
|
|
100
|
+
]
|
|
101
|
+
SecItemDelete(deleteQuery as CFDictionary)
|
|
102
|
+
|
|
103
|
+
// Add new entry with appropriate accessibility
|
|
104
|
+
let addQuery: [String: Any] = [
|
|
105
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
106
|
+
kSecAttrService as String: keychainService,
|
|
107
|
+
kSecAttrAccount as String: keychainAccount,
|
|
108
|
+
kSecValueData as String: data,
|
|
109
|
+
// This ensures data persists across reinstalls but stays on device (not synced)
|
|
110
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
|
114
|
+
|
|
115
|
+
if status != errSecSuccess {
|
|
116
|
+
// Log error but don't crash - we'll fall back to UserDefaults on next launch
|
|
117
|
+
print("Failed to save device ID to Keychain: \(status)")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -67,6 +67,51 @@ extension GetChannel {
|
|
|
67
67
|
return dict
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
struct ChannelInfo: Codable {
|
|
71
|
+
let id: String?
|
|
72
|
+
let name: String?
|
|
73
|
+
let `public`: Bool?
|
|
74
|
+
let allow_self_set: Bool?
|
|
75
|
+
}
|
|
76
|
+
struct ListChannelsDec: Decodable {
|
|
77
|
+
let channels: [ChannelInfo]?
|
|
78
|
+
let error: String?
|
|
79
|
+
|
|
80
|
+
init(from decoder: Decoder) throws {
|
|
81
|
+
let container = try decoder.singleValueContainer()
|
|
82
|
+
|
|
83
|
+
if let channelsArray = try? container.decode([ChannelInfo].self) {
|
|
84
|
+
// Backend returns direct array
|
|
85
|
+
self.channels = channelsArray
|
|
86
|
+
self.error = nil
|
|
87
|
+
} else {
|
|
88
|
+
// Handle error response
|
|
89
|
+
let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
|
|
90
|
+
self.channels = nil
|
|
91
|
+
self.error = try? errorContainer.decode(String.self, forKey: .error)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private enum CodingKeys: String, CodingKey {
|
|
96
|
+
case error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
public class ListChannels: NSObject {
|
|
100
|
+
var channels: [[String: Any]] = []
|
|
101
|
+
var error: String = ""
|
|
102
|
+
}
|
|
103
|
+
extension ListChannels {
|
|
104
|
+
func toDict() -> [String: Any] {
|
|
105
|
+
var dict: [String: Any] = [String: Any]()
|
|
106
|
+
let otherSelf: Mirror = Mirror(reflecting: self)
|
|
107
|
+
for child: Mirror.Child in otherSelf.children {
|
|
108
|
+
if let key: String = child.label {
|
|
109
|
+
dict[key] = child.value
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dict
|
|
113
|
+
}
|
|
114
|
+
}
|
|
70
115
|
struct InfoObject: Codable {
|
|
71
116
|
let platform: String?
|
|
72
117
|
let device_id: String?
|
|
@@ -112,6 +157,7 @@ struct AppVersionDec: Decodable {
|
|
|
112
157
|
let error: String?
|
|
113
158
|
let session_key: String?
|
|
114
159
|
let major: Bool?
|
|
160
|
+
let breaking: Bool?
|
|
115
161
|
let data: [String: String]?
|
|
116
162
|
let manifest: [ManifestEntry]?
|
|
117
163
|
}
|
|
@@ -124,6 +170,7 @@ public class AppVersion: NSObject {
|
|
|
124
170
|
var error: String?
|
|
125
171
|
var sessionKey: String?
|
|
126
172
|
var major: Bool?
|
|
173
|
+
var breaking: Bool?
|
|
127
174
|
var data: [String: String]?
|
|
128
175
|
var manifest: [ManifestEntry]?
|
|
129
176
|
}
|