react-native-security-suite 0.9.22 → 1.0.0-rc.2

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 (189) hide show
  1. package/README.md +291 -69
  2. package/android/build.gradle +11 -0
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/securitysuite/CryptoConfig.java +106 -0
  5. package/android/src/main/java/com/securitysuite/CryptoUtils.java +155 -0
  6. package/android/src/main/java/com/securitysuite/EcdhKeyStore.java +60 -0
  7. package/android/src/main/java/com/securitysuite/HeaderSanitizer.java +75 -0
  8. package/android/src/main/java/com/securitysuite/JWSGenerator.java +237 -32
  9. package/android/src/main/java/com/securitysuite/JwsFetchPayload.java +81 -0
  10. package/android/src/main/java/com/securitysuite/Obfuscation.java +57 -0
  11. package/android/src/main/java/com/securitysuite/SecureStorageNative.java +211 -0
  12. package/android/src/main/java/com/securitysuite/SecureView.java +2 -10
  13. package/android/src/main/java/com/securitysuite/SecureWindowHelper.java +30 -0
  14. package/android/src/main/java/com/securitysuite/SecuritySuiteModule.java +317 -102
  15. package/android/src/main/java/com/securitysuite/Sslpinning.java +219 -106
  16. package/android/src/main/java/com/securitysuite/security/AppIntegrityChecker.java +133 -0
  17. package/android/src/main/java/com/securitysuite/security/EmulatorDetector.java +145 -0
  18. package/android/src/main/java/com/securitysuite/security/RuntimeDetector.java +234 -0
  19. package/android/src/test/java/com/securitysuite/JWSGeneratorTest.java +153 -0
  20. package/android/src/test/java/com/securitysuite/SecureStorageNativeTest.java +37 -0
  21. package/ios/CryptoConfig.swift +73 -0
  22. package/ios/JWSGenerator.swift +288 -0
  23. package/ios/JWSGeneratorTests.swift +168 -0
  24. package/ios/KeychainHelper.swift +104 -0
  25. package/ios/Obfuscation.swift +42 -0
  26. package/ios/SecureStorageNative.swift +84 -0
  27. package/ios/Security/AppIntegrityChecker.swift +85 -0
  28. package/ios/Security/EmulatorDetector.swift +45 -0
  29. package/ios/Security/RuntimeDetector.swift +107 -0
  30. package/ios/SecuritySuite.mm +28 -4
  31. package/ios/SecuritySuite.swift +427 -134
  32. package/ios/SslPinning.swift +242 -263
  33. package/lib/commonjs/clipboard/index.js +3 -0
  34. package/lib/commonjs/clipboard/index.js.map +1 -0
  35. package/lib/commonjs/crypto/index.js +29 -0
  36. package/lib/commonjs/crypto/index.js.map +1 -0
  37. package/lib/commonjs/device/index.js +40 -0
  38. package/lib/commonjs/device/index.js.map +1 -0
  39. package/lib/commonjs/errors.js +62 -0
  40. package/lib/commonjs/errors.js.map +1 -0
  41. package/lib/commonjs/index.js +220 -151
  42. package/lib/commonjs/index.js.map +1 -1
  43. package/lib/commonjs/integrity/index.js +40 -0
  44. package/lib/commonjs/integrity/index.js.map +1 -0
  45. package/lib/commonjs/jws.js +141 -0
  46. package/lib/commonjs/jws.js.map +1 -0
  47. package/lib/commonjs/legacy/cryptoOptions.js +29 -0
  48. package/lib/commonjs/legacy/cryptoOptions.js.map +1 -0
  49. package/lib/commonjs/native/bridge.js +23 -0
  50. package/lib/commonjs/native/bridge.js.map +1 -0
  51. package/lib/commonjs/network/index.js +3 -0
  52. package/lib/commonjs/network/index.js.map +1 -0
  53. package/lib/commonjs/risk/score.js +36 -0
  54. package/lib/commonjs/risk/score.js.map +1 -0
  55. package/lib/commonjs/runtime/index.js +31 -0
  56. package/lib/commonjs/runtime/index.js.map +1 -0
  57. package/lib/commonjs/screen/index.js +13 -0
  58. package/lib/commonjs/screen/index.js.map +1 -0
  59. package/lib/commonjs/securitySuite/index.js +42 -0
  60. package/lib/commonjs/securitySuite/index.js.map +1 -0
  61. package/lib/commonjs/storage/index.js +3 -0
  62. package/lib/commonjs/storage/index.js.map +1 -0
  63. package/lib/commonjs/types/detection.js +2 -0
  64. package/lib/commonjs/types/detection.js.map +1 -0
  65. package/lib/module/clipboard/index.js +3 -0
  66. package/lib/module/clipboard/index.js.map +1 -0
  67. package/lib/module/crypto/index.js +25 -0
  68. package/lib/module/crypto/index.js.map +1 -0
  69. package/lib/module/device/index.js +36 -0
  70. package/lib/module/device/index.js.map +1 -0
  71. package/lib/module/errors.js +55 -0
  72. package/lib/module/errors.js.map +1 -0
  73. package/lib/module/index.js +147 -148
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/integrity/index.js +36 -0
  76. package/lib/module/integrity/index.js.map +1 -0
  77. package/lib/module/jws.js +127 -0
  78. package/lib/module/jws.js.map +1 -0
  79. package/lib/module/legacy/cryptoOptions.js +25 -0
  80. package/lib/module/legacy/cryptoOptions.js.map +1 -0
  81. package/lib/module/native/bridge.js +19 -0
  82. package/lib/module/native/bridge.js.map +1 -0
  83. package/lib/module/network/index.js +3 -0
  84. package/lib/module/network/index.js.map +1 -0
  85. package/lib/module/risk/score.js +32 -0
  86. package/lib/module/risk/score.js.map +1 -0
  87. package/lib/module/runtime/index.js +27 -0
  88. package/lib/module/runtime/index.js.map +1 -0
  89. package/lib/module/screen/index.js +5 -0
  90. package/lib/module/screen/index.js.map +1 -0
  91. package/lib/module/securitySuite/index.js +38 -0
  92. package/lib/module/securitySuite/index.js.map +1 -0
  93. package/lib/module/storage/index.js +3 -0
  94. package/lib/module/storage/index.js.map +1 -0
  95. package/lib/module/types/detection.js +2 -0
  96. package/lib/module/types/detection.js.map +1 -0
  97. package/lib/typescript/commonjs/docs/api-v1-proposal.d.ts +215 -0
  98. package/lib/typescript/commonjs/docs/api-v1-proposal.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/src/SecureView.d.ts +1 -1
  100. package/lib/typescript/commonjs/src/SecureView.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/src/clipboard/index.d.ts +2 -0
  102. package/lib/typescript/commonjs/src/clipboard/index.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/src/crypto/index.d.ts +15 -0
  104. package/lib/typescript/commonjs/src/crypto/index.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/src/device/index.d.ts +11 -0
  106. package/lib/typescript/commonjs/src/device/index.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/src/errors.d.ts +17 -0
  108. package/lib/typescript/commonjs/src/errors.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/src/helpers.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/src/index.d.ts +77 -24
  111. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/src/integrity/index.d.ts +6 -0
  113. package/lib/typescript/commonjs/src/integrity/index.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/src/jws.d.ts +44 -0
  115. package/lib/typescript/commonjs/src/jws.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/src/legacy/cryptoOptions.d.ts +35 -0
  117. package/lib/typescript/commonjs/src/legacy/cryptoOptions.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/src/native/bridge.d.ts +12 -0
  119. package/lib/typescript/commonjs/src/native/bridge.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/src/network/index.d.ts +2 -0
  121. package/lib/typescript/commonjs/src/network/index.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/src/risk/score.d.ts +12 -0
  123. package/lib/typescript/commonjs/src/risk/score.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/src/runtime/index.d.ts +6 -0
  125. package/lib/typescript/commonjs/src/runtime/index.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/src/screen/index.d.ts +3 -0
  127. package/lib/typescript/commonjs/src/screen/index.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/src/securitySuite/index.d.ts +6 -0
  129. package/lib/typescript/commonjs/src/securitySuite/index.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/src/storage/index.d.ts +2 -0
  131. package/lib/typescript/commonjs/src/storage/index.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/src/types/detection.d.ts +41 -0
  133. package/lib/typescript/commonjs/src/types/detection.d.ts.map +1 -0
  134. package/lib/typescript/module/docs/api-v1-proposal.d.ts +215 -0
  135. package/lib/typescript/module/docs/api-v1-proposal.d.ts.map +1 -0
  136. package/lib/typescript/module/src/SecureView.d.ts +1 -1
  137. package/lib/typescript/module/src/SecureView.d.ts.map +1 -1
  138. package/lib/typescript/module/src/clipboard/index.d.ts +2 -0
  139. package/lib/typescript/module/src/clipboard/index.d.ts.map +1 -0
  140. package/lib/typescript/module/src/crypto/index.d.ts +15 -0
  141. package/lib/typescript/module/src/crypto/index.d.ts.map +1 -0
  142. package/lib/typescript/module/src/device/index.d.ts +11 -0
  143. package/lib/typescript/module/src/device/index.d.ts.map +1 -0
  144. package/lib/typescript/module/src/errors.d.ts +17 -0
  145. package/lib/typescript/module/src/errors.d.ts.map +1 -0
  146. package/lib/typescript/module/src/helpers.d.ts.map +1 -1
  147. package/lib/typescript/module/src/index.d.ts +77 -24
  148. package/lib/typescript/module/src/index.d.ts.map +1 -1
  149. package/lib/typescript/module/src/integrity/index.d.ts +6 -0
  150. package/lib/typescript/module/src/integrity/index.d.ts.map +1 -0
  151. package/lib/typescript/module/src/jws.d.ts +44 -0
  152. package/lib/typescript/module/src/jws.d.ts.map +1 -0
  153. package/lib/typescript/module/src/legacy/cryptoOptions.d.ts +35 -0
  154. package/lib/typescript/module/src/legacy/cryptoOptions.d.ts.map +1 -0
  155. package/lib/typescript/module/src/native/bridge.d.ts +12 -0
  156. package/lib/typescript/module/src/native/bridge.d.ts.map +1 -0
  157. package/lib/typescript/module/src/network/index.d.ts +2 -0
  158. package/lib/typescript/module/src/network/index.d.ts.map +1 -0
  159. package/lib/typescript/module/src/risk/score.d.ts +12 -0
  160. package/lib/typescript/module/src/risk/score.d.ts.map +1 -0
  161. package/lib/typescript/module/src/runtime/index.d.ts +6 -0
  162. package/lib/typescript/module/src/runtime/index.d.ts.map +1 -0
  163. package/lib/typescript/module/src/screen/index.d.ts +3 -0
  164. package/lib/typescript/module/src/screen/index.d.ts.map +1 -0
  165. package/lib/typescript/module/src/securitySuite/index.d.ts +6 -0
  166. package/lib/typescript/module/src/securitySuite/index.d.ts.map +1 -0
  167. package/lib/typescript/module/src/storage/index.d.ts +2 -0
  168. package/lib/typescript/module/src/storage/index.d.ts.map +1 -0
  169. package/lib/typescript/module/src/types/detection.d.ts +41 -0
  170. package/lib/typescript/module/src/types/detection.d.ts.map +1 -0
  171. package/package.json +2 -10
  172. package/src/clipboard/index.ts +1 -0
  173. package/src/crypto/index.ts +40 -0
  174. package/src/device/index.ts +47 -0
  175. package/src/errors.ts +84 -0
  176. package/src/index.tsx +293 -195
  177. package/src/integrity/index.ts +46 -0
  178. package/src/jws.ts +213 -0
  179. package/src/legacy/cryptoOptions.ts +84 -0
  180. package/src/native/bridge.ts +37 -0
  181. package/src/network/index.ts +1 -0
  182. package/src/risk/score.ts +49 -0
  183. package/src/runtime/index.ts +43 -0
  184. package/src/screen/index.ts +2 -0
  185. package/src/securitySuite/index.ts +45 -0
  186. package/src/storage/index.ts +1 -0
  187. package/src/types/detection.ts +46 -0
  188. package/android/src/main/java/com/securitysuite/StorageEncryption.java +0 -52
  189. package/ios/StorageEncryption.swift +0 -89
@@ -0,0 +1,288 @@
1
+ import Foundation
2
+ import CryptoKit
3
+
4
+ @available(iOS 13.0, *)
5
+ struct JWSGenerator {
6
+ private static let safeHeaderKey = try! NSRegularExpression(pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$")
7
+ private static let safeHeaderValue = try! NSRegularExpression(pattern: "^[\\x20-\\x7E]+$")
8
+
9
+ static func validateAlgorithm(_ algorithm: String?) throws -> String {
10
+ let value = algorithm ?? "HS256"
11
+ switch value {
12
+ case "HS256", "HS384", "HS512":
13
+ return value
14
+ default:
15
+ throw NSError(
16
+ domain: "JWS",
17
+ code: 1,
18
+ userInfo: [NSLocalizedDescriptionKey: "Unsupported JWS algorithm: \(value)"]
19
+ )
20
+ }
21
+ }
22
+
23
+ static func validateHeaderKey(_ key: String) throws {
24
+ let range = NSRange(key.startIndex..., in: key)
25
+ guard safeHeaderKey.firstMatch(in: key, range: range) != nil else {
26
+ throw NSError(domain: "JWS", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid JWS header key: \(key)"])
27
+ }
28
+ }
29
+
30
+ static func validateHeaderValue(_ value: String) throws {
31
+ let range = NSRange(value.startIndex..., in: value)
32
+ guard safeHeaderValue.firstMatch(in: value, range: range) != nil else {
33
+ throw NSError(domain: "JWS", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid JWS header value"])
34
+ }
35
+ }
36
+
37
+ static func resolveAlgorithm(_ algorithm: String?, headers: [String: Any]) throws -> String {
38
+ let headerAlg = headers["alg"] as? String
39
+
40
+ if let algorithm, let headerAlg, algorithm != headerAlg {
41
+ throw NSError(
42
+ domain: "JWS",
43
+ code: 7,
44
+ userInfo: [NSLocalizedDescriptionKey: "JWS algorithm mismatch: options.algorithm and headers.alg must match"]
45
+ )
46
+ }
47
+
48
+ if let algorithm, !algorithm.isEmpty {
49
+ return try validateAlgorithm(algorithm)
50
+ }
51
+
52
+ if let headerAlg, !headerAlg.isEmpty {
53
+ return try validateAlgorithm(headerAlg)
54
+ }
55
+
56
+ return "HS256"
57
+ }
58
+
59
+ static func generate(
60
+ payloadString: String,
61
+ secret: String,
62
+ algorithm: String?,
63
+ headers: [String: Any]?,
64
+ detached: Bool
65
+ ) throws -> String {
66
+ guard !secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
67
+ throw NSError(
68
+ domain: "JWS",
69
+ code: 8,
70
+ userInfo: [NSLocalizedDescriptionKey: "JWS secret is required and must be a non-empty string"]
71
+ )
72
+ }
73
+
74
+ let normalizedHeaders = headers ?? [:]
75
+ let alg = try resolveAlgorithm(algorithm, headers: normalizedHeaders)
76
+ let protectedHeader = try buildProtectedHeader(algorithm: alg, headers: normalizedHeaders)
77
+
78
+ let encodedProtectedHeader = base64URLEncode(protectedHeader)
79
+ let encodedPayload = encodePayload(payloadString)
80
+ let signingInput = buildSigningInput(encodedProtectedHeader: encodedProtectedHeader, encodedPayload: encodedPayload)
81
+
82
+ let signature = try sign(
83
+ data: Data(signingInput.utf8),
84
+ secret: Data(secret.utf8),
85
+ algorithm: alg
86
+ )
87
+ let encodedSignature = base64URLEncode(signature)
88
+
89
+ return formatCompactJws(
90
+ encodedProtectedHeader: encodedProtectedHeader,
91
+ encodedPayload: encodedPayload,
92
+ encodedSignature: encodedSignature,
93
+ detached: detached
94
+ )
95
+ }
96
+
97
+ static func verify(
98
+ compactJWS: String,
99
+ payloadString: String,
100
+ secret: String,
101
+ algorithm: String?,
102
+ detached: Bool
103
+ ) throws -> Bool {
104
+ let encodedProtectedHeader: String
105
+ let encodedSignature: String
106
+
107
+ if detached {
108
+ let parts = compactJWS.components(separatedBy: "..")
109
+ guard parts.count == 2 else {
110
+ throw NSError(domain: "JWS", code: 6, userInfo: [NSLocalizedDescriptionKey: "Invalid compact detached JWS format"])
111
+ }
112
+ encodedProtectedHeader = parts[0]
113
+ encodedSignature = parts[1]
114
+ } else {
115
+ let parts = compactJWS.split(separator: ".", omittingEmptySubsequences: false)
116
+ guard parts.count == 3 else {
117
+ throw NSError(domain: "JWS", code: 6, userInfo: [NSLocalizedDescriptionKey: "Invalid compact JWS format"])
118
+ }
119
+ encodedProtectedHeader = String(parts[0])
120
+ encodedSignature = String(parts[2])
121
+ }
122
+
123
+ let alg = try validateAlgorithm(algorithm)
124
+ let encodedPayload = encodePayload(payloadString)
125
+ let signingInput = buildSigningInput(encodedProtectedHeader: encodedProtectedHeader, encodedPayload: encodedPayload)
126
+ let expected = try sign(data: Data(signingInput.utf8), secret: Data(secret.utf8), algorithm: alg)
127
+
128
+ guard let provided = Data(base64URLEncoded: encodedSignature) else {
129
+ return false
130
+ }
131
+ return expected == provided
132
+ }
133
+
134
+ private static func buildProtectedHeader(
135
+ algorithm: String,
136
+ headers: [String: Any]
137
+ ) throws -> Data {
138
+ var headerFields: [String: Any] = ["alg": algorithm]
139
+
140
+ let sortedKeys = headers.keys.sorted()
141
+ for key in sortedKeys {
142
+ if key == "alg" { continue }
143
+ try validateHeaderKey(key)
144
+ let value = headers[key]
145
+
146
+ if value is NSNull {
147
+ headerFields[key] = NSNull()
148
+ } else if let boolValue = value as? Bool {
149
+ headerFields[key] = boolValue
150
+ } else if let numberValue = value as? NSNumber {
151
+ headerFields[key] = numberValue
152
+ } else if let stringValue = value as? String {
153
+ try validateHeaderValue(stringValue)
154
+ headerFields[key] = stringValue
155
+ } else {
156
+ throw NSError(
157
+ domain: "JWS",
158
+ code: 9,
159
+ userInfo: [NSLocalizedDescriptionKey: "JWS header values must be JSON-serializable primitives: \(key)"]
160
+ )
161
+ }
162
+ }
163
+
164
+ return try JSONSerialization.data(withJSONObject: headerFields, options: [.sortedKeys])
165
+ }
166
+
167
+ static func encodePayload(_ payloadString: String) -> String {
168
+ if payloadString.isEmpty {
169
+ return ""
170
+ }
171
+ return base64URLEncode(Data(payloadString.utf8))
172
+ }
173
+
174
+ static func buildSigningInput(encodedProtectedHeader: String, encodedPayload: String) -> String {
175
+ encodedProtectedHeader + "." + encodedPayload
176
+ }
177
+
178
+ static func formatCompactJws(
179
+ encodedProtectedHeader: String,
180
+ encodedPayload: String,
181
+ encodedSignature: String,
182
+ detached: Bool
183
+ ) -> String {
184
+ if detached {
185
+ return "\(encodedProtectedHeader)..\(encodedSignature)"
186
+ }
187
+ return "\(encodedProtectedHeader).\(encodedPayload).\(encodedSignature)"
188
+ }
189
+
190
+ private static func sign(data: Data, secret: Data, algorithm: String) throws -> Data {
191
+ let key = SymmetricKey(data: secret)
192
+ switch algorithm {
193
+ case "HS384":
194
+ return Data(HMAC<SHA384>.authenticationCode(for: data, using: key))
195
+ case "HS512":
196
+ return Data(HMAC<SHA512>.authenticationCode(for: data, using: key))
197
+ default:
198
+ return Data(HMAC<SHA256>.authenticationCode(for: data, using: key))
199
+ }
200
+ }
201
+
202
+ private static func base64URLEncode(_ data: Data) -> String {
203
+ data.base64EncodedString()
204
+ .replacingOccurrences(of: "+", with: "-")
205
+ .replacingOccurrences(of: "/", with: "_")
206
+ .replacingOccurrences(of: "=", with: "")
207
+ }
208
+ }
209
+
210
+ @available(iOS 13.0, *)
211
+ enum JwsFetchPayload {
212
+ static func build(
213
+ url: String,
214
+ method: String,
215
+ requestBody: Data?,
216
+ jwsOptions: [String: Any]
217
+ ) throws -> String {
218
+ if let payload = jwsOptions["payload"] {
219
+ if payload is NSNull {
220
+ return ""
221
+ }
222
+ if let payloadString = payload as? String {
223
+ return payloadString
224
+ }
225
+ throw NSError(
226
+ domain: "JWS",
227
+ code: 10,
228
+ userInfo: [NSLocalizedDescriptionKey: "JWS fetch payload must be a string when provided"]
229
+ )
230
+ }
231
+
232
+ guard let requestUrl = URL(string: url) else {
233
+ throw NSError(domain: "JWS", code: 11, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
234
+ }
235
+
236
+ var fields: [String: Any] = [
237
+ "method": method.uppercased(),
238
+ "path": requestUrl.path.isEmpty ? "/" : requestUrl.path,
239
+ ]
240
+
241
+ if let query = requestUrl.query, !query.isEmpty {
242
+ fields["query"] = query
243
+ }
244
+
245
+ if let requestBody, !requestBody.isEmpty {
246
+ let digest = SHA256.hash(data: requestBody)
247
+ fields["bodyHash"] = base64URLEncode(Data(digest))
248
+ }
249
+
250
+ if let headers = jwsOptions["headers"] as? [String: Any] {
251
+ for key in ["timestamp", "nonce", "request_id", "requestId"] {
252
+ if let value = headers[key], !(value is NSNull) {
253
+ fields[key] = value
254
+ }
255
+ }
256
+ }
257
+
258
+ let sortedKeys = fields.keys.sorted()
259
+ var sortedFields: [String: Any] = [:]
260
+ for key in sortedKeys {
261
+ sortedFields[key] = fields[key]
262
+ }
263
+
264
+ let data = try JSONSerialization.data(withJSONObject: sortedFields, options: [.sortedKeys])
265
+ return String(decoding: data, as: UTF8.self)
266
+ }
267
+
268
+ private static func base64URLEncode(_ data: Data) -> String {
269
+ data.base64EncodedString()
270
+ .replacingOccurrences(of: "+", with: "-")
271
+ .replacingOccurrences(of: "/", with: "_")
272
+ .replacingOccurrences(of: "=", with: "")
273
+ }
274
+ }
275
+
276
+ @available(iOS 13.0, *)
277
+ private extension Data {
278
+ init?(base64URLEncoded string: String) {
279
+ var base64 = string
280
+ .replacingOccurrences(of: "-", with: "+")
281
+ .replacingOccurrences(of: "_", with: "/")
282
+ let padding = 4 - base64.count % 4
283
+ if padding < 4 {
284
+ base64 += String(repeating: "=", count: padding)
285
+ }
286
+ self.init(base64Encoded: base64)
287
+ }
288
+ }
@@ -0,0 +1,168 @@
1
+ import XCTest
2
+
3
+ @available(iOS 13.0, *)
4
+ final class JWSGeneratorTests: XCTestCase {
5
+ private let secret = "secret"
6
+
7
+ func testOmittedPayloadProducesThreeSegmentsWithEmptyMiddle() throws {
8
+ try assertEmptyPayload("")
9
+ }
10
+
11
+ func testEmptyStringPayloadProducesThreeSegmentsWithEmptyMiddle() throws {
12
+ try assertEmptyPayload("")
13
+ }
14
+
15
+ func testStringNullPayloadIsNotEmpty() throws {
16
+ let jws = try JWSGenerator.generate(
17
+ payloadString: "null",
18
+ secret: secret,
19
+ algorithm: "HS256",
20
+ headers: ["kid": "test-key"],
21
+ detached: false
22
+ )
23
+ let segments = jws.split(separator: ".", omittingEmptySubsequences: false)
24
+
25
+ XCTAssertEqual(segments.count, 3)
26
+ XCTAssertFalse(String(segments[1]).isEmpty)
27
+ XCTAssertEqual(String(segments[1]), base64URLEncode(Data("null".utf8)))
28
+ }
29
+
30
+ func testStringUndefinedPayloadIsNotEmpty() throws {
31
+ let jws = try JWSGenerator.generate(
32
+ payloadString: "undefined",
33
+ secret: secret,
34
+ algorithm: "HS256",
35
+ headers: ["kid": "test-key"],
36
+ detached: false
37
+ )
38
+ let segments = jws.split(separator: ".", omittingEmptySubsequences: false)
39
+
40
+ XCTAssertEqual(segments.count, 3)
41
+ XCTAssertEqual(String(segments[1]), base64URLEncode(Data("undefined".utf8)))
42
+ }
43
+
44
+ func testObjectPayloadIsBase64UrlEncodedJson() throws {
45
+ let payload = "{\"amount\":1000}"
46
+ let jws = try JWSGenerator.generate(
47
+ payloadString: payload,
48
+ secret: secret,
49
+ algorithm: "HS256",
50
+ headers: ["kid": "test-key"],
51
+ detached: false
52
+ )
53
+ let segments = jws.split(separator: ".", omittingEmptySubsequences: false)
54
+
55
+ XCTAssertEqual(segments.count, 3)
56
+ XCTAssertEqual(String(segments[1]), base64URLEncode(Data(payload.utf8)))
57
+ }
58
+
59
+ func testAlgorithmMismatchThrows() {
60
+ XCTAssertThrowsError(
61
+ try JWSGenerator.generate(
62
+ payloadString: "",
63
+ secret: secret,
64
+ algorithm: "HS512",
65
+ headers: ["alg": "HS256", "kid": "test-key"],
66
+ detached: false
67
+ )
68
+ )
69
+ }
70
+
71
+ func testAlgorithmFromHeaderIsUsed() throws {
72
+ let jws = try JWSGenerator.generate(
73
+ payloadString: "",
74
+ secret: secret,
75
+ algorithm: nil,
76
+ headers: ["alg": "HS384", "kid": "test-key"],
77
+ detached: false
78
+ )
79
+ let protectedHeader = String(jws.split(separator: ".", omittingEmptySubsequences: false)[0])
80
+ let decoded = try JSONSerialization.jsonObject(
81
+ with: Data(base64: base64URLToBase64(protectedHeader)),
82
+ options: []
83
+ ) as? [String: Any]
84
+
85
+ XCTAssertEqual(decoded?["alg"] as? String, "HS384")
86
+ }
87
+
88
+ func testDefaultAlgorithmIsHs256() throws {
89
+ let jws = try JWSGenerator.generate(
90
+ payloadString: "",
91
+ secret: secret,
92
+ algorithm: nil,
93
+ headers: ["kid": "test-key"],
94
+ detached: false
95
+ )
96
+ let protectedHeader = String(jws.split(separator: ".", omittingEmptySubsequences: false)[0])
97
+ let decoded = try JSONSerialization.jsonObject(
98
+ with: Data(base64: base64URLToBase64(protectedHeader)),
99
+ options: []
100
+ ) as? [String: Any]
101
+
102
+ XCTAssertEqual(decoded?["alg"] as? String, "HS256")
103
+ }
104
+
105
+ func testDetachedOutputUsesDoubleDot() throws {
106
+ let jws = try JWSGenerator.generate(
107
+ payloadString: "payload",
108
+ secret: secret,
109
+ algorithm: "HS256",
110
+ headers: ["kid": "test-key"],
111
+ detached: true
112
+ )
113
+
114
+ XCTAssertTrue(jws.contains(".."))
115
+ XCTAssertEqual(jws.components(separatedBy: "..").count, 2)
116
+ XCTAssertTrue(
117
+ try JWSGenerator.verify(
118
+ compactJWS: jws,
119
+ payloadString: "payload",
120
+ secret: secret,
121
+ algorithm: "HS256",
122
+ detached: true
123
+ )
124
+ )
125
+ }
126
+
127
+ private func assertEmptyPayload(_ payload: String) throws {
128
+ let jws = try JWSGenerator.generate(
129
+ payloadString: payload,
130
+ secret: secret,
131
+ algorithm: "HS256",
132
+ headers: ["kid": "test-key"],
133
+ detached: false
134
+ )
135
+ let segments = jws.split(separator: ".", omittingEmptySubsequences: false)
136
+
137
+ XCTAssertEqual(segments.count, 3)
138
+ XCTAssertEqual(String(segments[1]), "")
139
+ XCTAssertTrue(jws.contains(".."))
140
+ XCTAssertTrue(
141
+ try JWSGenerator.verify(
142
+ compactJWS: jws,
143
+ payloadString: payload,
144
+ secret: secret,
145
+ algorithm: "HS256",
146
+ detached: false
147
+ )
148
+ )
149
+ }
150
+
151
+ private func base64URLEncode(_ data: Data) -> String {
152
+ data.base64EncodedString()
153
+ .replacingOccurrences(of: "+", with: "-")
154
+ .replacingOccurrences(of: "/", with: "_")
155
+ .replacingOccurrences(of: "=", with: "")
156
+ }
157
+
158
+ private func base64URLToBase64(_ value: String) -> String {
159
+ var base64 = value
160
+ .replacingOccurrences(of: "-", with: "+")
161
+ .replacingOccurrences(of: "_", with: "/")
162
+ let padding = 4 - base64.count % 4
163
+ if padding < 4 {
164
+ base64 += String(repeating: "=", count: padding)
165
+ }
166
+ return base64
167
+ }
168
+ }
@@ -0,0 +1,104 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ enum KeychainError: Error {
5
+ case unexpectedStatus(OSStatus)
6
+ case invalidData
7
+ }
8
+
9
+ @available(iOS 13.0, *)
10
+ final class KeychainHelper {
11
+ static let shared = KeychainHelper()
12
+
13
+ private init() {}
14
+
15
+ func save(_ data: Data, service: String, account: String) throws {
16
+ let query: [String: Any] = [
17
+ kSecClass as String: kSecClassGenericPassword,
18
+ kSecAttrService as String: service,
19
+ kSecAttrAccount as String: account,
20
+ ]
21
+ SecItemDelete(query as CFDictionary)
22
+
23
+ var attributes = query
24
+ attributes[kSecValueData as String] = data
25
+ attributes[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
26
+
27
+ let status = SecItemAdd(attributes as CFDictionary, nil)
28
+ guard status == errSecSuccess else {
29
+ throw KeychainError.unexpectedStatus(status)
30
+ }
31
+ }
32
+
33
+ func load(service: String, account: String) throws -> Data? {
34
+ let query: [String: Any] = [
35
+ kSecClass as String: kSecClassGenericPassword,
36
+ kSecAttrService as String: service,
37
+ kSecAttrAccount as String: account,
38
+ kSecReturnData as String: true,
39
+ kSecMatchLimit as String: kSecMatchLimitOne,
40
+ ]
41
+
42
+ var result: AnyObject?
43
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
44
+ if status == errSecItemNotFound {
45
+ return nil
46
+ }
47
+ guard status == errSecSuccess, let data = result as? Data else {
48
+ throw KeychainError.unexpectedStatus(status)
49
+ }
50
+ return data
51
+ }
52
+
53
+ func delete(service: String, account: String) throws {
54
+ let query: [String: Any] = [
55
+ kSecClass as String: kSecClassGenericPassword,
56
+ kSecAttrService as String: service,
57
+ kSecAttrAccount as String: account,
58
+ ]
59
+ let status = SecItemDelete(query as CFDictionary)
60
+ guard status == errSecSuccess || status == errSecItemNotFound else {
61
+ throw KeychainError.unexpectedStatus(status)
62
+ }
63
+ }
64
+
65
+ func listAccounts(service: String) throws -> [String] {
66
+ let query: [String: Any] = [
67
+ kSecClass as String: kSecClassGenericPassword,
68
+ kSecAttrService as String: service,
69
+ kSecReturnAttributes as String: true,
70
+ kSecMatchLimit as String: kSecMatchLimitAll,
71
+ ]
72
+
73
+ var result: AnyObject?
74
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
75
+ if status == errSecItemNotFound {
76
+ return []
77
+ }
78
+ guard status == errSecSuccess else {
79
+ throw KeychainError.unexpectedStatus(status)
80
+ }
81
+
82
+ if let items = result as? [[String: Any]] {
83
+ return items.compactMap { $0[kSecAttrAccount as String] as? String }
84
+ }
85
+ if let item = result as? [String: Any],
86
+ let account = item[kSecAttrAccount as String] as? String {
87
+ return [account]
88
+ }
89
+ return []
90
+ }
91
+
92
+ func saveECDHKeyPair(privateKey: Data, publicKey: Data) throws {
93
+ try save(privateKey, service: "com.securitysuite.ecdh", account: "private")
94
+ try save(publicKey, service: "com.securitysuite.ecdh", account: "public")
95
+ }
96
+
97
+ func loadECDHKeyPair() throws -> (privateKey: Data, publicKey: Data)? {
98
+ guard let privateKey = try load(service: "com.securitysuite.ecdh", account: "private"),
99
+ let publicKey = try load(service: "com.securitysuite.ecdh", account: "public") else {
100
+ return nil
101
+ }
102
+ return (privateKey, publicKey)
103
+ }
104
+ }
@@ -0,0 +1,42 @@
1
+ import Foundation
2
+ import CryptoKit
3
+
4
+ @available(iOS 13.0, *)
5
+ final class Obfuscation {
6
+ private func deriveKey(_ secret: String) throws -> SymmetricKey {
7
+ guard !secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
8
+ throw NSError(
9
+ domain: "Obfuscation",
10
+ code: 1,
11
+ userInfo: [NSLocalizedDescriptionKey: "Obfuscation secret is required"]
12
+ )
13
+ }
14
+ let hash = SHA256.hash(data: Data(secret.utf8))
15
+ return SymmetricKey(data: Data(hash))
16
+ }
17
+
18
+ func obfuscate(plain: String, secret: String) throws -> String {
19
+ let symmetricKey = try deriveKey(secret)
20
+ guard let plainData = plain.data(using: .utf8) else {
21
+ throw NSError(domain: "Obfuscation", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid plaintext"])
22
+ }
23
+ let encrypted = try AES.GCM.seal(plainData, using: symmetricKey)
24
+ guard let combined = encrypted.combined else {
25
+ throw NSError(domain: "Obfuscation", code: 3, userInfo: [NSLocalizedDescriptionKey: "Obfuscation failed"])
26
+ }
27
+ return combined.base64EncodedString()
28
+ }
29
+
30
+ func deobfuscate(encoded: String, secret: String) throws -> String {
31
+ guard let decodedData = Data(base64Encoded: encoded) else {
32
+ throw NSError(domain: "Obfuscation", code: 4, userInfo: [NSLocalizedDescriptionKey: "Invalid obfuscated payload"])
33
+ }
34
+ let symmetricKey = try deriveKey(secret)
35
+ let sealedBox = try AES.GCM.SealedBox(combined: decodedData)
36
+ let decrypted = try AES.GCM.open(sealedBox, using: symmetricKey)
37
+ guard let result = String(data: decrypted, encoding: .utf8) else {
38
+ throw NSError(domain: "Obfuscation", code: 5, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 output"])
39
+ }
40
+ return result
41
+ }
42
+ }
@@ -0,0 +1,84 @@
1
+ import Foundation
2
+
3
+ @available(iOS 13.0, *)
4
+ final class SecureStorageNative {
5
+ static let shared = SecureStorageNative()
6
+ private let service = "com.securitysuite.secure_storage"
7
+ private let keyPrefix = "rss:"
8
+ private let keychain = KeychainHelper.shared
9
+
10
+ private init() {}
11
+
12
+ private func namespacedKey(_ key: String) throws -> String {
13
+ let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
14
+ guard !trimmed.isEmpty else {
15
+ throw secureStorageError("Storage key is required")
16
+ }
17
+ return "\(keyPrefix)\(trimmed)"
18
+ }
19
+
20
+ private func secureStorageError(_ detail: String) -> NSError {
21
+ NSError(
22
+ domain: "SecureStorage",
23
+ code: 1,
24
+ userInfo: [NSLocalizedDescriptionKey: "Secure storage operation failed: \(detail)"]
25
+ )
26
+ }
27
+
28
+ func setItem(key: String, value: String) throws {
29
+ let data = Data(value.utf8)
30
+ do {
31
+ try keychain.save(data, service: service, account: try namespacedKey(key))
32
+ } catch {
33
+ throw secureStorageError(error.localizedDescription)
34
+ }
35
+ }
36
+
37
+ func getItem(key: String) throws -> String? {
38
+ do {
39
+ guard let data = try keychain.load(service: service, account: try namespacedKey(key)) else {
40
+ return nil
41
+ }
42
+ guard let value = String(data: data, encoding: .utf8) else {
43
+ throw secureStorageError("Stored value is not valid UTF-8 text")
44
+ }
45
+ return value
46
+ } catch let error as NSError where error.domain == "SecureStorage" {
47
+ throw error
48
+ } catch {
49
+ throw secureStorageError(error.localizedDescription)
50
+ }
51
+ }
52
+
53
+ func removeItem(key: String) throws {
54
+ do {
55
+ try keychain.delete(service: service, account: try namespacedKey(key))
56
+ } catch {
57
+ throw secureStorageError(error.localizedDescription)
58
+ }
59
+ }
60
+
61
+ func getAllKeys() throws -> [String] {
62
+ do {
63
+ return try keychain
64
+ .listAccounts(service: service)
65
+ .filter { $0.hasPrefix(keyPrefix) }
66
+ .map { String($0.dropFirst(keyPrefix.count)) }
67
+ .sorted()
68
+ } catch {
69
+ throw secureStorageError(error.localizedDescription)
70
+ }
71
+ }
72
+
73
+ func clear() throws {
74
+ do {
75
+ for key in try getAllKeys() {
76
+ try removeItem(key: key)
77
+ }
78
+ } catch let error as NSError where error.domain == "SecureStorage" {
79
+ throw error
80
+ } catch {
81
+ throw secureStorageError(error.localizedDescription)
82
+ }
83
+ }
84
+ }