@capgo/capacitor-updater 8.0.1 → 8.2.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.
- package/CapgoCapacitorUpdater.podspec +7 -5
- package/Package.swift +9 -7
- package/README.md +984 -215
- package/android/build.gradle +24 -12
- package/android/proguard-rules.pro +22 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +110 -22
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1316 -489
- package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +662 -203
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +138 -33
- 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 +497 -133
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +80 -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 +873 -154
- package/dist/esm/definitions.d.ts +881 -114
- 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/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1610 -0
- package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +541 -231
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +286 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +54 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
- package/package.json +21 -19
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -975
- package/ios/Plugin/CryptoCipherV2.swift +0 -310
- /package/{LICENCE → LICENSE} +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
|
@@ -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?
|
|
@@ -83,6 +128,7 @@ struct InfoObject: Codable {
|
|
|
83
128
|
var action: String?
|
|
84
129
|
var channel: String?
|
|
85
130
|
var defaultChannel: String?
|
|
131
|
+
var key_id: String?
|
|
86
132
|
}
|
|
87
133
|
|
|
88
134
|
public struct ManifestEntry: Codable {
|
|
@@ -112,8 +158,12 @@ struct AppVersionDec: Decodable {
|
|
|
112
158
|
let error: String?
|
|
113
159
|
let session_key: String?
|
|
114
160
|
let major: Bool?
|
|
161
|
+
let breaking: Bool?
|
|
115
162
|
let data: [String: String]?
|
|
116
163
|
let manifest: [ManifestEntry]?
|
|
164
|
+
let link: String?
|
|
165
|
+
let comment: String?
|
|
166
|
+
// The HTTP status code is captured separately in CapgoUpdater; this struct only mirrors JSON.
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
public class AppVersion: NSObject {
|
|
@@ -124,8 +174,12 @@ public class AppVersion: NSObject {
|
|
|
124
174
|
var error: String?
|
|
125
175
|
var sessionKey: String?
|
|
126
176
|
var major: Bool?
|
|
177
|
+
var breaking: Bool?
|
|
127
178
|
var data: [String: String]?
|
|
128
179
|
var manifest: [ManifestEntry]?
|
|
180
|
+
var link: String?
|
|
181
|
+
var comment: String?
|
|
182
|
+
var statusCode: Int = 0
|
|
129
183
|
}
|
|
130
184
|
|
|
131
185
|
extension AppVersion {
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
import os.log
|
|
4
|
+
import WebKit
|
|
5
|
+
|
|
6
|
+
public class Logger {
|
|
7
|
+
public enum LogLevel: Int {
|
|
8
|
+
case silent = 0
|
|
9
|
+
case error
|
|
10
|
+
case warn
|
|
11
|
+
case info
|
|
12
|
+
case debug
|
|
13
|
+
|
|
14
|
+
public static subscript(_ str: String) -> LogLevel? {
|
|
15
|
+
var index = 0
|
|
16
|
+
|
|
17
|
+
while let item = LogLevel(rawValue: index) {
|
|
18
|
+
if str == "\(item)" {
|
|
19
|
+
return item
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
index += 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public func asOSLogType() -> OSLogType {
|
|
29
|
+
switch self {
|
|
30
|
+
case .debug:
|
|
31
|
+
return OSLogType.debug
|
|
32
|
+
case .info:
|
|
33
|
+
return OSLogType.info
|
|
34
|
+
case .warn:
|
|
35
|
+
return OSLogType.default
|
|
36
|
+
case .error:
|
|
37
|
+
return OSLogType.error
|
|
38
|
+
case .silent:
|
|
39
|
+
return OSLogType.info // Compiler wants this, it will never be used
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
public func asString() -> String {
|
|
43
|
+
switch self {
|
|
44
|
+
case .debug:
|
|
45
|
+
return "debug"
|
|
46
|
+
case .info:
|
|
47
|
+
return "info"
|
|
48
|
+
case .warn:
|
|
49
|
+
return "warn"
|
|
50
|
+
case .error:
|
|
51
|
+
return "error"
|
|
52
|
+
case .silent:
|
|
53
|
+
return "info"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public struct Options {
|
|
59
|
+
public var level: LogLevel
|
|
60
|
+
public var labels: [String: String]
|
|
61
|
+
public var useSyslog: Bool
|
|
62
|
+
|
|
63
|
+
public init(level: LogLevel = LogLevel.info, labels: [String: String] = [:], useSyslog: Bool = false) {
|
|
64
|
+
self.level = level
|
|
65
|
+
self.labels = labels
|
|
66
|
+
self.useSyslog = useSyslog
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private var _labels: [LogLevel: String] = [
|
|
71
|
+
LogLevel.silent: "",
|
|
72
|
+
LogLevel.error: "🔴",
|
|
73
|
+
LogLevel.warn: "🟠",
|
|
74
|
+
LogLevel.info: "🟢",
|
|
75
|
+
LogLevel.debug: "🔎"
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
public var labels: [String: String] {
|
|
79
|
+
get {
|
|
80
|
+
var result: [String: String] = [:]
|
|
81
|
+
|
|
82
|
+
for (level, label) in _labels {
|
|
83
|
+
result[String(describing: level)] = label
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
set {
|
|
90
|
+
for (level, label) in newValue {
|
|
91
|
+
if let logLevel = LogLevel[level] {
|
|
92
|
+
_labels[logLevel] = label
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public var level = LogLevel.info
|
|
99
|
+
|
|
100
|
+
public var levelName: String {
|
|
101
|
+
get {
|
|
102
|
+
String(describing: level)
|
|
103
|
+
}
|
|
104
|
+
set {
|
|
105
|
+
if let newLevel = LogLevel[newValue] {
|
|
106
|
+
level = newLevel
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private var _tag = ""
|
|
112
|
+
|
|
113
|
+
public var tag: String {
|
|
114
|
+
get {
|
|
115
|
+
_tag
|
|
116
|
+
}
|
|
117
|
+
set {
|
|
118
|
+
if !newValue.isEmpty {
|
|
119
|
+
_tag = newValue
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private let kDefaultTimerLabel = "default"
|
|
125
|
+
private var timers: [String: Date] = [:]
|
|
126
|
+
public var useSyslog = false
|
|
127
|
+
public var webView: WKWebView?
|
|
128
|
+
|
|
129
|
+
public func setWebView(webView: WKWebView) {
|
|
130
|
+
self.webView = webView
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public init(
|
|
134
|
+
withTag tag: String,
|
|
135
|
+
config: InstanceConfiguration? = nil,
|
|
136
|
+
options: Options? = nil
|
|
137
|
+
) {
|
|
138
|
+
self.tag = tag
|
|
139
|
+
if let config = config {
|
|
140
|
+
// The logger plugin's name is LoggerBridge, we want to look at the config
|
|
141
|
+
// named "Logger", so we can't use plugin.getConfigValue().
|
|
142
|
+
if let configLevel = getConfigValue("level", from: config) as? String,
|
|
143
|
+
let logLevel = LogLevel[configLevel] {
|
|
144
|
+
level = logLevel
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if let configLabels = getConfigValue("labels", from: config) as? [String: String] {
|
|
148
|
+
labels = configLabels
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if let configSyslog = getConfigValue("useSyslog", from: config) as? Bool {
|
|
152
|
+
useSyslog = configSyslog
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if let options = options {
|
|
157
|
+
self.level = options.level
|
|
158
|
+
self.labels = options.labels
|
|
159
|
+
self.useSyslog = options.useSyslog
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func getConfigValue(_ configKey: String, from config: InstanceConfiguration) -> Any? {
|
|
164
|
+
if let config = config.pluginConfigurations as? JSObject {
|
|
165
|
+
return config[keyPath: KeyPath(stringLiteral: "Logger.\(configKey)")]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return nil
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public func error(_ message: String) {
|
|
172
|
+
log(atLevel: LogLevel.error, message: message)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public func warn(_ message: String) {
|
|
176
|
+
log(atLevel: LogLevel.warn, message: message)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public func info(_ message: String) {
|
|
180
|
+
log(atLevel: LogLevel.info, message: message)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public func log(_ message: String) {
|
|
184
|
+
log(atLevel: LogLevel.info, message: message)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public func debug(_ message: String) {
|
|
188
|
+
log(atLevel: LogLevel.debug, message: message)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public func dir(_ value: Any?) {
|
|
192
|
+
// Check the log level here to avoid conversion to string
|
|
193
|
+
if canLog(atLevel: LogLevel.info) {
|
|
194
|
+
log(atLevel: LogLevel.info, message: String(describing: value))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public func log(atLevel level: LogLevel, message: String) {
|
|
199
|
+
// This will never fail, but we have to keep swift happy
|
|
200
|
+
if let label = _labels[level] {
|
|
201
|
+
log(atLevel: level, label: label, tag: tag, message: message)
|
|
202
|
+
if let webView = self.webView {
|
|
203
|
+
DispatchQueue.main.async {
|
|
204
|
+
let combined = "\(label) \(self.tag) : \(message)"
|
|
205
|
+
let jsArg = self.toJSStringLiteral(combined)
|
|
206
|
+
webView.evaluateJavaScript("console.\(level.asString())(\(jsArg))", completionHandler: nil)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func toJSStringLiteral(_ value: String) -> String {
|
|
213
|
+
// Prefer JSON encoding to produce a valid JS string literal
|
|
214
|
+
if let data = try? JSONEncoder().encode(value),
|
|
215
|
+
let encoded = String(data: data, encoding: .utf8) {
|
|
216
|
+
return encoded
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Fallback manual escaping (unlikely to be used)
|
|
220
|
+
var s = value
|
|
221
|
+
s = s.replacingOccurrences(of: "\\", with: "\\\\")
|
|
222
|
+
s = s.replacingOccurrences(of: "\"", with: "\\\"")
|
|
223
|
+
s = s.replacingOccurrences(of: "'", with: "\\'")
|
|
224
|
+
s = s.replacingOccurrences(of: "\n", with: "\\n")
|
|
225
|
+
s = s.replacingOccurrences(of: "\r", with: "\\r")
|
|
226
|
+
s = s.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
|
|
227
|
+
s = s.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
|
|
228
|
+
return "\"\(s)\""
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public func log(atLevel level: LogLevel, label: String?, tag: String, message: String) {
|
|
232
|
+
// This will never fail, but we have to keep swift happy
|
|
233
|
+
if let label = label ?? _labels[level] {
|
|
234
|
+
print(atLevel: level, label: label, tag: tag, message: message)
|
|
235
|
+
// eval
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func canLog(atLevel level: LogLevel) -> Bool {
|
|
240
|
+
self.level.rawValue >= level.rawValue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private func print(atLevel level: LogLevel, label: String, tag: String, message: String) {
|
|
244
|
+
guard canLog(atLevel: level) else {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
var msg = message
|
|
249
|
+
|
|
250
|
+
if !label.isEmpty {
|
|
251
|
+
// If the label is ASCII, put it after the tag, otherwise before.
|
|
252
|
+
if label[label.startIndex].isASCII {
|
|
253
|
+
msg = "[\(tag)] \(label): \(message)"
|
|
254
|
+
} else {
|
|
255
|
+
msg = "\(label) [\(tag)]: \(message)"
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
msg = "[\(tag)]: \(message)"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if useSyslog {
|
|
262
|
+
os_log("%{public}@", type: level.asOSLogType(), msg)
|
|
263
|
+
} else {
|
|
264
|
+
Swift.print(msg)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public func time(_ label: String?) {
|
|
269
|
+
timers[label ?? ""] = Date()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public func timeLog(_ label: String?) {
|
|
273
|
+
if let timer = timers[label ?? ""] {
|
|
274
|
+
info(formatTimeInterval(timer.timeIntervalSinceNow))
|
|
275
|
+
} else {
|
|
276
|
+
warn("timer \(label ?? kDefaultTimerLabel) does not exist")
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public func timeEnd(_ label: String?) {
|
|
281
|
+
timeLog(label)
|
|
282
|
+
timers.removeValue(forKey: label ?? kDefaultTimerLabel)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private func formatTimeInterval(_ interval: TimeInterval) -> String {
|
|
286
|
+
let int = Int(interval)
|
|
287
|
+
let millis = Int(((1 + interval.remainder(dividingBy: 1)) * 1000).rounded())
|
|
288
|
+
let seconds = int % 60
|
|
289
|
+
let minutes = (int / 60) % 60
|
|
290
|
+
let hours = (int / 3600)
|
|
291
|
+
|
|
292
|
+
if seconds < 1 {
|
|
293
|
+
return "\(millis)ms"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if minutes < 1 {
|
|
297
|
+
return "\(seconds).\(String(format: "%0.3d", millis))s"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if hours < 1 {
|
|
301
|
+
return "\(minutes):\(String(format: "%0.2d", seconds)).\(String(format: "%0.3d", millis)) (min:sec.ms)"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return "\(hours):\(String(format: "%0.2d", minutes)):\(String(format: "%0.2d", seconds)) (hr:min:sec)"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public func trace() {
|
|
308
|
+
info(String(format: "%@", Thread.callStackSymbols))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import BigInt
|
|
2
|
+
import Foundation
|
|
3
|
+
import CommonCrypto
|
|
4
|
+
import CryptoKit
|
|
5
|
+
|
|
6
|
+
///
|
|
7
|
+
/// Constants
|
|
8
|
+
///
|
|
9
|
+
private enum RSAConstants {
|
|
10
|
+
static let rsaKeySizeInBits: NSNumber = 2048
|
|
11
|
+
static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// We do all this stuff because ios is shit and open source libraries allow to do decryption with public key
|
|
15
|
+
// So we have to do it manually, while in nodejs or Java it's ok and done at language level.
|
|
16
|
+
|
|
17
|
+
///
|
|
18
|
+
/// The RSA public key.
|
|
19
|
+
///
|
|
20
|
+
public struct RSAPublicKey {
|
|
21
|
+
private let manualKey: ManualRSAPublicKey
|
|
22
|
+
|
|
23
|
+
fileprivate init(manualKey: ManualRSAPublicKey) {
|
|
24
|
+
self.manualKey = manualKey
|
|
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
|
+
}
|
|
34
|
+
|
|
35
|
+
///
|
|
36
|
+
/// Allows you to load an RSA public key (i.e. one downloaded from the net).
|
|
37
|
+
///
|
|
38
|
+
public static func load(rsaPublicKey: String) -> RSAPublicKey? {
|
|
39
|
+
// Clean up the key string
|
|
40
|
+
var pubKey: String = rsaPublicKey
|
|
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 {
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
guard bytes[index] == 0x02 else {
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
index += 1
|
|
187
|
+
|
|
188
|
+
// Get exponent length
|
|
189
|
+
if index >= bytes.count {
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var exponentLength = 0
|
|
194
|
+
if bytes[index] & 0x80 != 0 {
|
|
195
|
+
let lenBytes = Int(bytes[index] & 0x7F)
|
|
196
|
+
if (index + 1 + lenBytes) >= bytes.count {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
index += 1
|
|
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 {
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let exponentData = Data(bytes[index..<(index + exponentLength)])
|
|
215
|
+
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Decrypt data using raw RSA operation (c^d mod n)
|
|
219
|
+
func decrypt(_ encryptedData: Data) -> Data? {
|
|
220
|
+
// Create positive BigInt from encrypted data
|
|
221
|
+
let encryptedBytes = [UInt8](encryptedData)
|
|
222
|
+
var encryptedValue = BigUInt(0)
|
|
223
|
+
for byte in encryptedBytes {
|
|
224
|
+
encryptedValue = (encryptedValue << 8) | BigUInt(byte)
|
|
225
|
+
}
|
|
226
|
+
let encrypted = BigInt(encryptedValue)
|
|
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 {
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Convert BigUInt to bytes with padding
|
|
242
|
+
var resultBytes = [UInt8]()
|
|
243
|
+
var tempValue = bigUIntValue
|
|
244
|
+
while tempValue > 0 {
|
|
245
|
+
let byte = UInt8(tempValue & 0xFF)
|
|
246
|
+
resultBytes.insert(byte, at: 0) // Prepend to get big-endian
|
|
247
|
+
tempValue >>= 8
|
|
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
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|