react-native-instantpay-code-push 1.1.8 → 1.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/InstantpayCodePush.podspec +5 -1
- package/ios/BundleFileStorageService.swift +1269 -0
- package/ios/BundleMetadata.swift +208 -0
- package/ios/DecompressService.swift +116 -0
- package/ios/FileManagerService.swift +104 -0
- package/ios/HashUtils.swift +73 -0
- package/ios/InstantpayCodePush-Bridging-Header.h +16 -0
- package/ios/InstantpayCodePush.h +39 -1
- package/ios/InstantpayCodePush.mm +332 -4
- package/ios/IpayCodePushHelper.swift +57 -0
- package/ios/IpayCodePushImpl.swift +297 -0
- package/ios/NotificationExtension.swift +13 -0
- package/ios/SignatureVerifier.swift +358 -0
- package/ios/URLSessionDownloadService.swift +251 -0
- package/ios/VersionedPreferencesService.swift +93 -0
- package/ios/ZipDecompressionStrategy.swift +175 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VersionedPreferencesService.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 09/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
enum PreferencesError: Error {
|
|
11
|
+
case configurationError
|
|
12
|
+
case setItemError(String)
|
|
13
|
+
case getItemError(String)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protocol PreferencesService {
|
|
17
|
+
func setItem(_ value: String?, forKey key: String) throws
|
|
18
|
+
func getItem(forKey key: String) throws -> String?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class VersionedPreferencesService: PreferencesService {
|
|
22
|
+
|
|
23
|
+
private let userDefaults: UserDefaults
|
|
24
|
+
private var isolationKey: String = ""
|
|
25
|
+
|
|
26
|
+
let CLASS_TAG = "*PreferencesService"
|
|
27
|
+
|
|
28
|
+
init(userDefaults: UserDefaults = .standard) {
|
|
29
|
+
self.userDefaults = userDefaults
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configures the service with isolation key.
|
|
34
|
+
* @param isolationKey The complete isolation key to use for storage
|
|
35
|
+
*/
|
|
36
|
+
func configure(isolationKey: String) {
|
|
37
|
+
self.isolationKey = isolationKey
|
|
38
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Configured with isolation key: \(self.isolationKey)")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a prefixed key for UserDefaults storage.
|
|
43
|
+
* @param key The base key to prefix
|
|
44
|
+
* @return The prefixed key
|
|
45
|
+
* @throws PreferencesError if configuration is missing
|
|
46
|
+
*/
|
|
47
|
+
private func prefixedKey(forKey key: String) throws -> String {
|
|
48
|
+
guard !isolationKey.isEmpty else {
|
|
49
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Warning: PreferencesService used before configure(isolationKey:) was called. Isolation key is empty.")
|
|
50
|
+
throw PreferencesError.configurationError
|
|
51
|
+
}
|
|
52
|
+
return "\(isolationKey)\(key)"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sets a value in preferences.
|
|
57
|
+
* @param value The value to store (or nil to remove)
|
|
58
|
+
* @param key The key to store under
|
|
59
|
+
* @throws PreferencesError if key prefixing fails
|
|
60
|
+
*/
|
|
61
|
+
func setItem(_ value: String?, forKey key: String) throws {
|
|
62
|
+
do {
|
|
63
|
+
let fullKey = try prefixedKey(forKey: key)
|
|
64
|
+
if let valueToSet = value {
|
|
65
|
+
userDefaults.set(valueToSet, forKey: fullKey)
|
|
66
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Set '\(fullKey)' = '\(valueToSet)'")
|
|
67
|
+
} else {
|
|
68
|
+
userDefaults.removeObject(forKey: fullKey)
|
|
69
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Removed '\(fullKey)'")
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Error setting key '\(key)': \(error)")
|
|
73
|
+
throw PreferencesError.setItemError(key)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets a value from preferences.
|
|
79
|
+
* @param key The key to retrieve
|
|
80
|
+
* @return The stored value or nil if not found
|
|
81
|
+
* @throws PreferencesError if key prefixing fails
|
|
82
|
+
*/
|
|
83
|
+
func getItem(forKey key: String) throws -> String? {
|
|
84
|
+
do {
|
|
85
|
+
let fullKey = try prefixedKey(forKey: key)
|
|
86
|
+
return userDefaults.string(forKey: fullKey)
|
|
87
|
+
} catch {
|
|
88
|
+
IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Error getting key '\(key)': \(error)")
|
|
89
|
+
throw PreferencesError.getItemError(key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ZipDecompressionStrategy.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 09/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import SWCompression
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Strategy for handling ZIP compressed files
|
|
13
|
+
*/
|
|
14
|
+
class ZipDecompressionStrategy: DecompressionStrategy {
|
|
15
|
+
|
|
16
|
+
private static let ZIP_MAGIC_NUMBER: [UInt8] = [0x50, 0x4B, 0x03, 0x04]
|
|
17
|
+
private static let MIN_ZIP_SIZE: UInt64 = 22
|
|
18
|
+
let CLASS_TAG = "ZipStrategy"
|
|
19
|
+
|
|
20
|
+
func isValid(file: String) -> Bool {
|
|
21
|
+
guard FileManager.default.fileExists(atPath: file) else {
|
|
22
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: file doesn't exist")
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
do {
|
|
27
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: file)
|
|
28
|
+
guard let fileSize = attributes[.size] as? UInt64, fileSize >= Self.MIN_ZIP_SIZE else {
|
|
29
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: file too small")
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read attributes - \(error.localizedDescription)")
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
guard let fileHandle = FileHandle(forReadingAtPath: file) else {
|
|
38
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot open file")
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
defer {
|
|
43
|
+
fileHandle.closeFile()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
guard let header = try? fileHandle.read(upToCount: 4), header.count == 4 else {
|
|
47
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read header")
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let magicBytes = [UInt8](header)
|
|
52
|
+
guard magicBytes == Self.ZIP_MAGIC_NUMBER else {
|
|
53
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: wrong magic number")
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard let zipData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
|
|
58
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read file data")
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
do {
|
|
63
|
+
_ = try ZipContainer.open(container: zipData)
|
|
64
|
+
return true
|
|
65
|
+
} catch {
|
|
66
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: structure validation failed - \(error.localizedDescription)")
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
72
|
+
|
|
73
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Starting extraction of \(file) to \(destination)")
|
|
74
|
+
|
|
75
|
+
guard let zipData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
|
|
76
|
+
throw NSError(
|
|
77
|
+
domain: "IpayCodePush",
|
|
78
|
+
code: 1,
|
|
79
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to read ZIP file at: \(file)"]
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
progressHandler(0.1)
|
|
84
|
+
|
|
85
|
+
let zipEntries: [ZipEntry]
|
|
86
|
+
do {
|
|
87
|
+
zipEntries = try ZipContainer.open(container: zipData)
|
|
88
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "ZIP extraction successful, found \(zipEntries.count) entries")
|
|
89
|
+
} catch {
|
|
90
|
+
throw NSError(
|
|
91
|
+
domain: "IpayCodePush",
|
|
92
|
+
code: 2,
|
|
93
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP extraction failed: \(error.localizedDescription)"]
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
progressHandler(0.2)
|
|
98
|
+
|
|
99
|
+
let destinationURL = URL(fileURLWithPath: destination)
|
|
100
|
+
let canonicalDestination = destinationURL.standardized.path
|
|
101
|
+
|
|
102
|
+
let fileManager = FileManager.default
|
|
103
|
+
if !fileManager.fileExists(atPath: canonicalDestination) {
|
|
104
|
+
try fileManager.createDirectory(
|
|
105
|
+
atPath: canonicalDestination,
|
|
106
|
+
withIntermediateDirectories: true,
|
|
107
|
+
attributes: nil
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let totalEntries = Double(zipEntries.count)
|
|
112
|
+
for (index, entry) in zipEntries.enumerated() {
|
|
113
|
+
try extractZipEntry(entry, to: canonicalDestination)
|
|
114
|
+
progressHandler(0.2 + (Double(index + 1) / totalEntries * 0.8))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Successfully extracted all entries")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func extractZipEntry(_ entry: ZipEntry, to destination: String) throws {
|
|
121
|
+
let fileManager = FileManager.default
|
|
122
|
+
let entryPath = entry.info.name.trimmingCharacters(in: .init(charactersIn: "/"))
|
|
123
|
+
|
|
124
|
+
guard !entryPath.isEmpty,
|
|
125
|
+
!entryPath.contains(".."),
|
|
126
|
+
!entryPath.hasPrefix("/") else {
|
|
127
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Skipping suspicious path: \(entry.info.name)")
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let fullPath = (destination as NSString).appendingPathComponent(entryPath)
|
|
132
|
+
let fullURL = URL(fileURLWithPath: fullPath)
|
|
133
|
+
let canonicalFullPath = fullURL.standardized.path
|
|
134
|
+
let canonicalDestination = URL(fileURLWithPath: destination).standardized.path
|
|
135
|
+
|
|
136
|
+
guard canonicalFullPath.hasPrefix(canonicalDestination + "/") ||
|
|
137
|
+
canonicalFullPath == canonicalDestination else {
|
|
138
|
+
throw NSError(
|
|
139
|
+
domain: "IpayCodePush",
|
|
140
|
+
code: 3,
|
|
141
|
+
userInfo: [NSLocalizedDescriptionKey: "Path traversal attempt detected: \(entry.info.name)"]
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if entry.info.type == .directory {
|
|
146
|
+
if !fileManager.fileExists(atPath: canonicalFullPath) {
|
|
147
|
+
try fileManager.createDirectory(
|
|
148
|
+
atPath: canonicalFullPath,
|
|
149
|
+
withIntermediateDirectories: true,
|
|
150
|
+
attributes: nil
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if entry.info.type == .regular {
|
|
157
|
+
let parentPath = (canonicalFullPath as NSString).deletingLastPathComponent
|
|
158
|
+
if !fileManager.fileExists(atPath: parentPath) {
|
|
159
|
+
try fileManager.createDirectory(
|
|
160
|
+
atPath: parentPath,
|
|
161
|
+
withIntermediateDirectories: true,
|
|
162
|
+
attributes: nil
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
guard let data = entry.data else {
|
|
167
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Skipping file with no data: \(entry.info.name)")
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try data.write(to: URL(fileURLWithPath: canonicalFullPath))
|
|
172
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Extracted: \(entryPath)")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|