react-native-security-suite 0.9.21 → 1.0.0-rc.1
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/README.md +233 -65
- package/android/build.gradle +11 -0
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/securitysuite/CryptoConfig.java +158 -0
- package/android/src/main/java/com/securitysuite/CryptoUtils.java +152 -0
- package/android/src/main/java/com/securitysuite/EcdhKeyStore.java +60 -0
- package/android/src/main/java/com/securitysuite/HeaderSanitizer.java +75 -0
- package/android/src/main/java/com/securitysuite/JWSGenerator.java +237 -32
- package/android/src/main/java/com/securitysuite/JwsFetchPayload.java +81 -0
- package/android/src/main/java/com/securitysuite/Obfuscation.java +57 -0
- package/android/src/main/java/com/securitysuite/SecureStorageNative.java +211 -0
- package/android/src/main/java/com/securitysuite/SecureView.java +2 -10
- package/android/src/main/java/com/securitysuite/SecureWindowHelper.java +30 -0
- package/android/src/main/java/com/securitysuite/SecuritySuiteModule.java +310 -102
- package/android/src/main/java/com/securitysuite/Sslpinning.java +219 -106
- package/android/src/main/java/com/securitysuite/security/AppIntegrityChecker.java +133 -0
- package/android/src/main/java/com/securitysuite/security/EmulatorDetector.java +145 -0
- package/android/src/main/java/com/securitysuite/security/RuntimeDetector.java +234 -0
- package/android/src/test/java/com/securitysuite/JWSGeneratorTest.java +153 -0
- package/android/src/test/java/com/securitysuite/SecureStorageNativeTest.java +37 -0
- package/ios/CryptoConfig.swift +124 -0
- package/ios/JWSGenerator.swift +288 -0
- package/ios/JWSGeneratorTests.swift +168 -0
- package/ios/KeychainHelper.swift +104 -0
- package/ios/Obfuscation.swift +42 -0
- package/ios/SecureStorageNative.swift +84 -0
- package/ios/Security/AppIntegrityChecker.swift +85 -0
- package/ios/Security/EmulatorDetector.swift +45 -0
- package/ios/Security/RuntimeDetector.swift +107 -0
- package/ios/SecuritySuite.mm +28 -4
- package/ios/SecuritySuite.swift +407 -131
- package/ios/SslPinning.swift +242 -263
- package/lib/commonjs/clipboard/index.js +3 -0
- package/lib/commonjs/clipboard/index.js.map +1 -0
- package/lib/commonjs/crypto/index.js +39 -0
- package/lib/commonjs/crypto/index.js.map +1 -0
- package/lib/commonjs/device/index.js +40 -0
- package/lib/commonjs/device/index.js.map +1 -0
- package/lib/commonjs/errors.js +62 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/index.js +220 -151
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/integrity/index.js +40 -0
- package/lib/commonjs/integrity/index.js.map +1 -0
- package/lib/commonjs/jws.js +141 -0
- package/lib/commonjs/jws.js.map +1 -0
- package/lib/commonjs/legacy/cryptoOptions.js +20 -0
- package/lib/commonjs/legacy/cryptoOptions.js.map +1 -0
- package/lib/commonjs/native/bridge.js +23 -0
- package/lib/commonjs/native/bridge.js.map +1 -0
- package/lib/commonjs/network/index.js +3 -0
- package/lib/commonjs/network/index.js.map +1 -0
- package/lib/commonjs/risk/score.js +36 -0
- package/lib/commonjs/risk/score.js.map +1 -0
- package/lib/commonjs/runtime/index.js +31 -0
- package/lib/commonjs/runtime/index.js.map +1 -0
- package/lib/commonjs/screen/index.js +13 -0
- package/lib/commonjs/screen/index.js.map +1 -0
- package/lib/commonjs/securitySuite/index.js +42 -0
- package/lib/commonjs/securitySuite/index.js.map +1 -0
- package/lib/commonjs/storage/index.js +3 -0
- package/lib/commonjs/storage/index.js.map +1 -0
- package/lib/commonjs/types/detection.js +2 -0
- package/lib/commonjs/types/detection.js.map +1 -0
- package/lib/module/clipboard/index.js +3 -0
- package/lib/module/clipboard/index.js.map +1 -0
- package/lib/module/crypto/index.js +35 -0
- package/lib/module/crypto/index.js.map +1 -0
- package/lib/module/device/index.js +36 -0
- package/lib/module/device/index.js.map +1 -0
- package/lib/module/errors.js +55 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +147 -148
- package/lib/module/index.js.map +1 -1
- package/lib/module/integrity/index.js +36 -0
- package/lib/module/integrity/index.js.map +1 -0
- package/lib/module/jws.js +127 -0
- package/lib/module/jws.js.map +1 -0
- package/lib/module/legacy/cryptoOptions.js +16 -0
- package/lib/module/legacy/cryptoOptions.js.map +1 -0
- package/lib/module/native/bridge.js +19 -0
- package/lib/module/native/bridge.js.map +1 -0
- package/lib/module/network/index.js +3 -0
- package/lib/module/network/index.js.map +1 -0
- package/lib/module/risk/score.js +32 -0
- package/lib/module/risk/score.js.map +1 -0
- package/lib/module/runtime/index.js +27 -0
- package/lib/module/runtime/index.js.map +1 -0
- package/lib/module/screen/index.js +5 -0
- package/lib/module/screen/index.js.map +1 -0
- package/lib/module/securitySuite/index.js +38 -0
- package/lib/module/securitySuite/index.js.map +1 -0
- package/lib/module/storage/index.js +3 -0
- package/lib/module/storage/index.js.map +1 -0
- package/lib/module/types/detection.js +2 -0
- package/lib/module/types/detection.js.map +1 -0
- package/lib/typescript/commonjs/docs/api-v1-proposal.d.ts +215 -0
- package/lib/typescript/commonjs/docs/api-v1-proposal.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SecureView.d.ts +1 -1
- package/lib/typescript/commonjs/src/SecureView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/clipboard/index.d.ts +2 -0
- package/lib/typescript/commonjs/src/clipboard/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/crypto/index.d.ts +15 -0
- package/lib/typescript/commonjs/src/crypto/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/device/index.d.ts +11 -0
- package/lib/typescript/commonjs/src/device/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/errors.d.ts +17 -0
- package/lib/typescript/commonjs/src/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/helpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +77 -24
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/integrity/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/integrity/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/jws.d.ts +44 -0
- package/lib/typescript/commonjs/src/jws.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/legacy/cryptoOptions.d.ts +35 -0
- package/lib/typescript/commonjs/src/legacy/cryptoOptions.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/native/bridge.d.ts +12 -0
- package/lib/typescript/commonjs/src/native/bridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/network/index.d.ts +2 -0
- package/lib/typescript/commonjs/src/network/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/risk/score.d.ts +12 -0
- package/lib/typescript/commonjs/src/risk/score.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/runtime/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/runtime/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/screen/index.d.ts +3 -0
- package/lib/typescript/commonjs/src/screen/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/securitySuite/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/securitySuite/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/storage/index.d.ts +2 -0
- package/lib/typescript/commonjs/src/storage/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types/detection.d.ts +41 -0
- package/lib/typescript/commonjs/src/types/detection.d.ts.map +1 -0
- package/lib/typescript/module/docs/api-v1-proposal.d.ts +215 -0
- package/lib/typescript/module/docs/api-v1-proposal.d.ts.map +1 -0
- package/lib/typescript/module/src/SecureView.d.ts +1 -1
- package/lib/typescript/module/src/SecureView.d.ts.map +1 -1
- package/lib/typescript/module/src/clipboard/index.d.ts +2 -0
- package/lib/typescript/module/src/clipboard/index.d.ts.map +1 -0
- package/lib/typescript/module/src/crypto/index.d.ts +15 -0
- package/lib/typescript/module/src/crypto/index.d.ts.map +1 -0
- package/lib/typescript/module/src/device/index.d.ts +11 -0
- package/lib/typescript/module/src/device/index.d.ts.map +1 -0
- package/lib/typescript/module/src/errors.d.ts +17 -0
- package/lib/typescript/module/src/errors.d.ts.map +1 -0
- package/lib/typescript/module/src/helpers.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +77 -24
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/integrity/index.d.ts +6 -0
- package/lib/typescript/module/src/integrity/index.d.ts.map +1 -0
- package/lib/typescript/module/src/jws.d.ts +44 -0
- package/lib/typescript/module/src/jws.d.ts.map +1 -0
- package/lib/typescript/module/src/legacy/cryptoOptions.d.ts +35 -0
- package/lib/typescript/module/src/legacy/cryptoOptions.d.ts.map +1 -0
- package/lib/typescript/module/src/native/bridge.d.ts +12 -0
- package/lib/typescript/module/src/native/bridge.d.ts.map +1 -0
- package/lib/typescript/module/src/network/index.d.ts +2 -0
- package/lib/typescript/module/src/network/index.d.ts.map +1 -0
- package/lib/typescript/module/src/risk/score.d.ts +12 -0
- package/lib/typescript/module/src/risk/score.d.ts.map +1 -0
- package/lib/typescript/module/src/runtime/index.d.ts +6 -0
- package/lib/typescript/module/src/runtime/index.d.ts.map +1 -0
- package/lib/typescript/module/src/screen/index.d.ts +3 -0
- package/lib/typescript/module/src/screen/index.d.ts.map +1 -0
- package/lib/typescript/module/src/securitySuite/index.d.ts +6 -0
- package/lib/typescript/module/src/securitySuite/index.d.ts.map +1 -0
- package/lib/typescript/module/src/storage/index.d.ts +2 -0
- package/lib/typescript/module/src/storage/index.d.ts.map +1 -0
- package/lib/typescript/module/src/types/detection.d.ts +41 -0
- package/lib/typescript/module/src/types/detection.d.ts.map +1 -0
- package/package.json +2 -4
- package/src/clipboard/index.ts +1 -0
- package/src/crypto/index.ts +49 -0
- package/src/device/index.ts +47 -0
- package/src/errors.ts +84 -0
- package/src/index.tsx +293 -195
- package/src/integrity/index.ts +46 -0
- package/src/jws.ts +213 -0
- package/src/legacy/cryptoOptions.ts +49 -0
- package/src/native/bridge.ts +37 -0
- package/src/network/index.ts +1 -0
- package/src/risk/score.ts +49 -0
- package/src/runtime/index.ts +43 -0
- package/src/screen/index.ts +2 -0
- package/src/securitySuite/index.ts +45 -0
- package/src/storage/index.ts +1 -0
- package/src/types/detection.ts +46 -0
- package/android/src/main/java/com/securitysuite/StorageEncryption.java +0 -52
- 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
|
+
}
|