@capgo/capacitor-updater 7.51.15 → 7.51.16
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 +0 -2
- package/Package.swift +2 -4
- package/android/build.gradle +6 -6
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +0 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +18 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +14 -10
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/package.json +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -16,7 +16,5 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.dependency 'ZIPFoundation', '~> 0.9'
|
|
17
17
|
s.dependency 'Alamofire', '5.10.2'
|
|
18
18
|
s.dependency 'Version', '0.8.0'
|
|
19
|
-
# We cannot update to latest because it not published anymore in cocoa https://github.com/attaswift/BigInt/issues/94
|
|
20
|
-
s.dependency 'BigInt', '5.2.0'
|
|
21
19
|
s.swift_version = '5.1'
|
|
22
20
|
end
|
package/Package.swift
CHANGED
|
@@ -13,8 +13,7 @@ let package = Package(
|
|
|
13
13
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"),
|
|
14
14
|
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.12.0")),
|
|
15
15
|
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"),
|
|
16
|
-
.package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
|
|
17
|
-
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0")
|
|
16
|
+
.package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
|
|
18
17
|
],
|
|
19
18
|
targets: [
|
|
20
19
|
.target(
|
|
@@ -24,8 +23,7 @@ let package = Package(
|
|
|
24
23
|
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
25
24
|
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
|
|
26
25
|
.product(name: "Alamofire", package: "Alamofire"),
|
|
27
|
-
.product(name: "Version", package: "Version")
|
|
28
|
-
.product(name: "BigInt", package: "BigInt")
|
|
26
|
+
.product(name: "Version", package: "Version")
|
|
29
27
|
],
|
|
30
28
|
path: "ios/Sources/CapacitorUpdaterPlugin"),
|
|
31
29
|
.testTarget(
|
package/android/build.gradle
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
ext {
|
|
2
2
|
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
-
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.
|
|
4
|
-
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.
|
|
5
|
-
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
okhttpVersion = project.hasProperty('okhttpVersion') ? rootProject.ext.okhttpVersion : '4.12.0'
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
buildscript {
|
|
@@ -54,7 +55,6 @@ dependencies {
|
|
|
54
55
|
implementation "androidx.work:work-runtime:$work_version"
|
|
55
56
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
|
56
57
|
implementation "com.google.android.gms:play-services-tasks:18.4.1"
|
|
57
|
-
implementation "com.google.guava:guava:33.6.0-android"
|
|
58
58
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
59
59
|
implementation project(':capacitor-android')
|
|
60
60
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
@@ -65,11 +65,11 @@ dependencies {
|
|
|
65
65
|
testImplementation "junit:junit:$junitVersion"
|
|
66
66
|
testImplementation 'org.mockito:mockito-core:5.23.0'
|
|
67
67
|
testImplementation 'org.json:json:20250517'
|
|
68
|
-
testImplementation 'org.robolectric:robolectric:4.
|
|
68
|
+
testImplementation 'org.robolectric:robolectric:4.17'
|
|
69
69
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
70
70
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
71
71
|
implementation 'org.brotli:dec:0.1.2'
|
|
72
|
-
implementation
|
|
72
|
+
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
tasks.withType(Test) {
|
|
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
148
|
|
|
149
|
-
private final String pluginVersion = "7.51.
|
|
149
|
+
private final String pluginVersion = "7.51.16";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -23,8 +23,6 @@ import androidx.work.OneTimeWorkRequest;
|
|
|
23
23
|
import androidx.work.PeriodicWorkRequest;
|
|
24
24
|
import androidx.work.WorkInfo;
|
|
25
25
|
import androidx.work.WorkManager;
|
|
26
|
-
import com.google.common.util.concurrent.Futures;
|
|
27
|
-
import com.google.common.util.concurrent.ListenableFuture;
|
|
28
26
|
import java.io.BufferedInputStream;
|
|
29
27
|
import java.io.BufferedReader;
|
|
30
28
|
import java.io.File;
|
|
@@ -75,6 +75,17 @@ public class DelayUpdateUtils {
|
|
|
75
75
|
", longValue: " +
|
|
76
76
|
longValue
|
|
77
77
|
);
|
|
78
|
+
} else {
|
|
79
|
+
delayConditionListToKeep.add(condition);
|
|
80
|
+
logger.info(
|
|
81
|
+
"Background delay (value: " +
|
|
82
|
+
value +
|
|
83
|
+
") condition kept at index " +
|
|
84
|
+
index +
|
|
85
|
+
" (source: " +
|
|
86
|
+
source.toString() +
|
|
87
|
+
")"
|
|
88
|
+
);
|
|
78
89
|
}
|
|
79
90
|
} else {
|
|
80
91
|
delayConditionListToKeep.add(condition);
|
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "7.51.
|
|
99
|
+
private let pluginVersion: String = "7.51.16"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -219,6 +219,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
219
219
|
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
220
220
|
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
221
221
|
private var pendingNotifyAppReady = false
|
|
222
|
+
private let semaphoreWaitTestingLock = NSLock()
|
|
223
|
+
private var didEnterSemaphoreWaitForTesting = false
|
|
222
224
|
|
|
223
225
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
224
226
|
|
|
@@ -597,6 +599,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
597
599
|
|
|
598
600
|
private func semaphoreWait(waitTime: Int) {
|
|
599
601
|
// print("\\(CapgoUpdater.TAG) semaphoreWait \\(waitTime)")
|
|
602
|
+
semaphoreWaitTestingLock.lock()
|
|
603
|
+
didEnterSemaphoreWaitForTesting = true
|
|
604
|
+
semaphoreWaitTestingLock.unlock()
|
|
600
605
|
let result = semaphoreReady.wait(timeout: .now() + .milliseconds(waitTime))
|
|
601
606
|
if result == .timedOut {
|
|
602
607
|
logger.error("Semaphore wait timed out after \(waitTime)ms")
|
|
@@ -4108,6 +4113,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4108
4113
|
pendingNotifyAppReady = true
|
|
4109
4114
|
}
|
|
4110
4115
|
|
|
4116
|
+
func resetSemaphoreWaitTestingStateForTesting() {
|
|
4117
|
+
semaphoreWaitTestingLock.lock()
|
|
4118
|
+
didEnterSemaphoreWaitForTesting = false
|
|
4119
|
+
semaphoreWaitTestingLock.unlock()
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
var didEnterSemaphoreWaitForTestingState: Bool {
|
|
4123
|
+
semaphoreWaitTestingLock.lock()
|
|
4124
|
+
defer { semaphoreWaitTestingLock.unlock() }
|
|
4125
|
+
return didEnterSemaphoreWaitForTesting
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4111
4128
|
func clearPendingNotifyAppReadyForTesting() {
|
|
4112
4129
|
clearPendingNotifyAppReady()
|
|
4113
4130
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import os.log
|
|
10
|
+
|
|
11
|
+
/// Isolated raw RSA primitives for Capgo v2 checksum/session-key recovery.
|
|
12
|
+
/// SonarCloud automatic analysis has no Swift // NOSONAR; swift:S5542 is ignored
|
|
13
|
+
/// via `.sonarcloud.properties` multicriteria because `.rsaEncryptionRaw` is
|
|
14
|
+
/// intentional wire compatibility with Node `privateEncrypt` / `publicDecrypt`.
|
|
15
|
+
enum CapgoRawRsa {
|
|
16
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
17
|
+
|
|
18
|
+
static func supportsCapgoRecovery(key: SecKey) -> Bool {
|
|
19
|
+
SecKeyGetBlockSize(key) == 256
|
|
20
|
+
&& SecKeyIsAlgorithmSupported(key, .encrypt, .rsaEncryptionRaw) // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func recoverSignatureBlock(key: SecKey, data: Data) -> Data? {
|
|
24
|
+
var error: Unmanaged<CFError>?
|
|
25
|
+
guard let recovered = SecKeyCreateEncryptedData(key, .rsaEncryptionRaw, data as CFData, &error) as Data? else { // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
26
|
+
os_log(
|
|
27
|
+
"RSA raw recovery failed: %{public}@",
|
|
28
|
+
log: log,
|
|
29
|
+
type: .error,
|
|
30
|
+
String(describing: error?.takeRetainedValue())
|
|
31
|
+
)
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
return recovered
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -928,10 +928,7 @@ import UIKit
|
|
|
928
928
|
|
|
929
929
|
struct ManifestLookupEntry {
|
|
930
930
|
let hash: String
|
|
931
|
-
/// The manifest's own file name, `.br` suffix included when present.
|
|
932
|
-
/// built-in bundle stores files under this exact name (see
|
|
933
|
-
/// isManifestEntryAvailableLocally), unlike the extracted/cached copy which
|
|
934
|
-
/// is always named without the suffix.
|
|
931
|
+
/// The manifest's own file name, `.br` suffix included when present.
|
|
935
932
|
let originalFileName: String
|
|
936
933
|
}
|
|
937
934
|
|
|
@@ -994,10 +991,14 @@ import UIKit
|
|
|
994
991
|
|
|
995
992
|
// Builtin is already a permanent reuse source (see isManifestEntryAvailableLocally),
|
|
996
993
|
// so there's no need to also duplicate this file into the delta cache
|
|
997
|
-
let
|
|
998
|
-
let
|
|
999
|
-
let
|
|
1000
|
-
|
|
994
|
+
let builtinLookupName = knownEntry?.originalFileName ?? relativePath
|
|
995
|
+
let isBuiltinOrigin: Bool
|
|
996
|
+
if let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: builtinLookupName) {
|
|
997
|
+
isBuiltinOrigin = fileManager.fileExists(atPath: builtinFilePath.path) &&
|
|
998
|
+
verifyChecksum(file: builtinFilePath, expectedHash: checksum)
|
|
999
|
+
} else {
|
|
1000
|
+
isBuiltinOrigin = false
|
|
1001
|
+
}
|
|
1001
1002
|
if isBuiltinOrigin {
|
|
1002
1003
|
continue
|
|
1003
1004
|
}
|
|
@@ -1231,7 +1232,10 @@ import UIKit
|
|
|
1231
1232
|
}
|
|
1232
1233
|
|
|
1233
1234
|
let builtinFolder = self.builtinFolderURL()
|
|
1234
|
-
|
|
1235
|
+
// The .br suffix describes transport; builtin assets are uncompressed.
|
|
1236
|
+
guard let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName) else {
|
|
1237
|
+
return false
|
|
1238
|
+
}
|
|
1235
1239
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
1236
1240
|
return true
|
|
1237
1241
|
}
|
|
@@ -1474,7 +1478,7 @@ import UIKit
|
|
|
1474
1478
|
let builtinFilePath: URL
|
|
1475
1479
|
do {
|
|
1476
1480
|
destFilePath = try Self.resolveManifestTargetPath(baseDirectory: destFolder, fileName: fileName)
|
|
1477
|
-
builtinFilePath = try Self.
|
|
1481
|
+
builtinFilePath = try Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName)
|
|
1478
1482
|
} catch {
|
|
1479
1483
|
logger.error("Invalid manifest file path: \(fileName)")
|
|
1480
1484
|
self.sendStats(action: "manifest_path_fail", versionName: "\(version):\(fileName)")
|
|
@@ -1,274 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
///
|
|
7
|
-
/// Constants
|
|
8
|
-
///
|
|
9
|
-
private enum RSAConstants {
|
|
10
|
-
static let rsaKeySizeInBits: NSNumber = 2048
|
|
11
|
-
static let rsaAlgorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
|
|
12
|
-
}
|
|
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
|
+
*/
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import os.log
|
|
16
10
|
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
11
|
+
/// Capgo v2 uses Node's privateEncrypt with PKCS#1 v1.5 type-1 padding for
|
|
12
|
+
/// checksums and session keys. Recover the signed payload using Apple's RSA
|
|
13
|
+
/// implementation instead of custom modular exponentiation.
|
|
20
14
|
public struct RSAPublicKey {
|
|
21
|
-
private let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
}
|
|
15
|
+
private let key: SecKey
|
|
16
|
+
private static let keyLock = NSLock()
|
|
17
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
18
|
+
// One public key only: bounded storage, invalid/rotated keys never fall back
|
|
19
|
+
// to a cached key with different material. SecKey is immutable after import.
|
|
20
|
+
private static var cachedKey: (der: Data, key: SecKey)?
|
|
34
21
|
|
|
35
|
-
///
|
|
36
|
-
/// Allows you to load an RSA public key (i.e. one downloaded from the net).
|
|
37
|
-
///
|
|
38
22
|
public static func load(rsaPublicKey: String) -> RSAPublicKey? {
|
|
39
|
-
|
|
40
|
-
|
|
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 {
|
|
23
|
+
guard let der = pemToDer(rsaPublicKey) else {
|
|
24
|
+
os_log("RSA key import failed: invalid PEM/base64", log: Self.log, type: .error)
|
|
180
25
|
return nil
|
|
181
26
|
}
|
|
182
27
|
|
|
183
|
-
|
|
28
|
+
keyLock.lock()
|
|
29
|
+
defer { keyLock.unlock() }
|
|
30
|
+
if let cached = cachedKey, cached.der == der {
|
|
31
|
+
return RSAPublicKey(key: cached.key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let attributes: [CFString: Any] = [
|
|
35
|
+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
|
36
|
+
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
37
|
+
]
|
|
38
|
+
var error: Unmanaged<CFError>?
|
|
39
|
+
guard let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error) else {
|
|
40
|
+
os_log(
|
|
41
|
+
"RSA key import failed: %{public}@",
|
|
42
|
+
log: Self.log,
|
|
43
|
+
type: .error,
|
|
44
|
+
String(describing: error?.takeRetainedValue())
|
|
45
|
+
)
|
|
184
46
|
return nil
|
|
185
47
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
48
|
+
guard CapgoRawRsa.supportsCapgoRecovery(key: key) else {
|
|
49
|
+
os_log(
|
|
50
|
+
"RSA key import failed: unsupported key (Capgo requires RSA-2048 with raw encryption)",
|
|
51
|
+
log: Self.log,
|
|
52
|
+
type: .error
|
|
53
|
+
)
|
|
190
54
|
return nil
|
|
191
55
|
}
|
|
56
|
+
cachedKey = (der, key)
|
|
57
|
+
return RSAPublicKey(key: key)
|
|
58
|
+
}
|
|
192
59
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 {
|
|
60
|
+
public func decrypt(data: Data) -> Data? {
|
|
61
|
+
let blockSize = SecKeyGetBlockSize(key)
|
|
62
|
+
guard data.count == blockSize else { return nil }
|
|
63
|
+
// Raw public-key encryption is the same c^e mod n operation as Node's
|
|
64
|
+
// publicDecrypt. Do NOT use encryption PKCS#1 padding here: the recovered
|
|
65
|
+
// block already contains signature (type-1) padding, checked below.
|
|
66
|
+
guard let recovered = CapgoRawRsa.recoverSignatureBlock(key: key, data: data) else {
|
|
211
67
|
return nil
|
|
212
68
|
}
|
|
213
|
-
|
|
214
|
-
let exponentData = Data(bytes[index..<(index + exponentLength)])
|
|
215
|
-
return ManualRSAPublicKey(modulus: modulusData, exponent: exponentData)
|
|
69
|
+
return Self.unpadSignature(recovered, blockSize: blockSize)
|
|
216
70
|
}
|
|
217
71
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
72
|
+
static func unpadSignature(_ block: Data, blockSize: Int = 256) -> Data? {
|
|
73
|
+
let bytes = [UInt8](block)
|
|
74
|
+
// RFC 8017: 00 01, at least eight FF bytes, 00, then the payload.
|
|
75
|
+
guard bytes.count == blockSize, bytes[0] == 0, bytes[1] == 1 else { return nil }
|
|
76
|
+
var separator = 2
|
|
77
|
+
while separator < bytes.count && bytes[separator] == 0xff {
|
|
78
|
+
separator += 1
|
|
225
79
|
}
|
|
226
|
-
|
|
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 {
|
|
80
|
+
guard separator >= 10, separator < bytes.count - 1, bytes[separator] == 0 else {
|
|
238
81
|
return nil
|
|
239
82
|
}
|
|
83
|
+
return Data(bytes[(separator + 1)...])
|
|
84
|
+
}
|
|
240
85
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
}
|
|
86
|
+
private static func pemToDer(_ pem: String) -> Data? {
|
|
87
|
+
let encoded = pem
|
|
88
|
+
.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
89
|
+
.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
90
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
91
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
92
|
+
.replacingOccurrences(of: "\\n", with: "")
|
|
93
|
+
.components(separatedBy: .whitespacesAndNewlines).joined()
|
|
94
|
+
return Data(base64Encoded: encoded)
|
|
273
95
|
}
|
|
274
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-updater",
|
|
3
|
-
"version": "7.51.
|
|
3
|
+
"version": "7.51.16",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Live update for capacitor apps",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"@ionic/eslint-config": "^0.4.0",
|
|
90
90
|
"@ionic/prettier-config": "^4.0.0",
|
|
91
91
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
92
|
-
"@types/node": "^
|
|
92
|
+
"@types/node": "^26.0.0",
|
|
93
93
|
"eslint": "^8.57.0",
|
|
94
94
|
"eslint-plugin-import": "^2.32.0",
|
|
95
95
|
"husky": "^9.1.7",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import BigInt
|
|
2
|
-
|
|
3
|
-
// Add this custom power function to ensure safer handling of power operations
|
|
4
|
-
|
|
5
|
-
// Manual exponentiation using the square-and-multiply algorithm
|
|
6
|
-
// which is more efficient and avoids using the built-in functions that might handle BigInt differently
|
|
7
|
-
extension BigInt {
|
|
8
|
-
func manualPower(_ exponent: BigInt, modulus: BigInt) -> BigInt {
|
|
9
|
-
// Quick checks
|
|
10
|
-
if modulus == 0 {
|
|
11
|
-
return 0
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if exponent == 0 {
|
|
15
|
-
return 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
guard let base = self.magnitude as? BigUInt,
|
|
19
|
-
let exp = exponent.magnitude as? BigUInt,
|
|
20
|
-
let mod = modulus.magnitude as? BigUInt else {
|
|
21
|
-
return 0
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Square and multiply algorithm for modular exponentiation
|
|
25
|
-
var result = BigUInt(1)
|
|
26
|
-
var currentBase = base % mod
|
|
27
|
-
var currentExp = exp
|
|
28
|
-
|
|
29
|
-
while currentExp > 0 {
|
|
30
|
-
if currentExp & 1 == 1 {
|
|
31
|
-
result = (result * currentBase) % mod
|
|
32
|
-
}
|
|
33
|
-
currentBase = (currentBase * currentBase) % mod
|
|
34
|
-
currentExp >>= 1
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return BigInt(result)
|
|
38
|
-
}
|
|
39
|
-
}
|