@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.
Files changed (55) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +9 -7
  3. package/README.md +984 -215
  4. package/android/build.gradle +24 -12
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +110 -22
  7. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1316 -489
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +662 -203
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +138 -33
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +497 -133
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +80 -25
  16. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  17. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  19. package/dist/docs.json +873 -154
  20. package/dist/esm/definitions.d.ts +881 -114
  21. package/dist/esm/definitions.js.map +1 -1
  22. package/dist/esm/history.d.ts +1 -0
  23. package/dist/esm/history.js +283 -0
  24. package/dist/esm/history.js.map +1 -0
  25. package/dist/esm/index.d.ts +1 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/web.d.ts +12 -1
  29. package/dist/esm/web.js +29 -2
  30. package/dist/esm/web.js.map +1 -1
  31. package/dist/plugin.cjs.js +311 -2
  32. package/dist/plugin.cjs.js.map +1 -1
  33. package/dist/plugin.js +311 -2
  34. package/dist/plugin.js.map +1 -1
  35. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  36. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  37. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
  38. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
  39. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1610 -0
  40. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +541 -231
  41. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +286 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  43. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  44. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +54 -0
  45. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  48. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  49. package/package.json +21 -19
  50. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -975
  51. package/ios/Plugin/CryptoCipherV2.swift +0 -310
  52. /package/{LICENCE → LICENSE} +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  55. /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
+ }