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.
Files changed (189) hide show
  1. package/README.md +233 -65
  2. package/android/build.gradle +11 -0
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/securitysuite/CryptoConfig.java +158 -0
  5. package/android/src/main/java/com/securitysuite/CryptoUtils.java +152 -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 +310 -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 +124 -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 +407 -131
  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 +39 -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 +20 -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 +35 -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 +16 -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 -4
  172. package/src/clipboard/index.ts +1 -0
  173. package/src/crypto/index.ts +49 -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 +49 -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
@@ -1,143 +1,384 @@
1
1
  import Foundation
2
2
  import CryptoKit
3
- import SwiftUI
4
3
  import Security
5
- import CommonCrypto
4
+ import UIKit
6
5
  import IOSSecuritySuite
7
6
 
8
7
  @objc(SecuritySuite)
9
8
  class SecuritySuite: NSObject {
10
- var privateKey: String!,
11
- publicKey: String!,
12
- sharedKey: String!,
13
- keyData: Data!,
14
- session: URLSession!
9
+ private let hkdfSalt = "react-native-security-suite".data(using: .utf8)!
10
+ private let hkdfInfoEncryption = "rss-encryption-v1".data(using: .utf8)!
11
+ private let hkdfInfoHmac = "rss-hmac-v1".data(using: .utf8)!
12
+
13
+ private var privateKeyData: Data?
14
+ private var encryptionKeyData: Data?
15
+ private var hmacKeyData: Data?
16
+ private var cryptoConfig = CryptoConfig.defaults()
17
+
18
+ private func loadOrCreateECDHKeyPair() throws -> P256.KeyAgreement.PrivateKey {
19
+ if let stored = try KeychainHelper.shared.loadECDHKeyPair() {
20
+ privateKeyData = stored.privateKey
21
+ return try P256.KeyAgreement.PrivateKey(derRepresentation: stored.privateKey)
22
+ }
23
+
24
+ let key = P256.KeyAgreement.PrivateKey()
25
+ privateKeyData = key.derRepresentation
26
+ try KeychainHelper.shared.saveECDHKeyPair(
27
+ privateKey: key.derRepresentation,
28
+ publicKey: key.publicKey.derRepresentation
29
+ )
30
+ return key
31
+ }
15
32
 
16
33
  @objc(getPublicKey:withRejecter:)
17
- func getPublicKey(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
34
+ func getPublicKey(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
18
35
  do {
19
- let key = P256.KeyAgreement.PrivateKey()
20
- privateKey = key.derRepresentation.base64EncodedString()
21
- publicKey = key.publicKey.derRepresentation.base64EncodedString()
22
-
23
- resolve(publicKey)
36
+ let key = try loadOrCreateECDHKeyPair()
37
+ resolve(key.publicKey.derRepresentation.base64EncodedString())
24
38
  } catch {
25
- reject("error", "GET_PUBLIC_KEY_ERROR", nil)
39
+ reject("GET_PUBLIC_KEY_ERROR", error.localizedDescription, error)
26
40
  }
27
41
  }
28
-
29
- @objc(getSharedKey:withResolver:withRejecter:)
30
- func getSharedKey(serverPK: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
42
+
43
+ @objc(establishSharedKey:withOptions:withResolver:withRejecter:)
44
+ func establishSharedKey(
45
+ serverPK: NSString,
46
+ options: NSDictionary?,
47
+ resolve: @escaping RCTPromiseResolveBlock,
48
+ reject: RCTPromiseRejectBlock
49
+ ) {
31
50
  do {
32
- guard let serverPublicKeyData = Data(base64Encoded: serverPK as String),
33
- let privateKeyData = Data(base64Encoded: privateKey as String) else { return }
51
+ cryptoConfig = try CryptoConfig.from(dictionary: options)
52
+
53
+ guard cryptoConfig.keyAgreementAlgorithm == "ECDH",
54
+ cryptoConfig.keyFactoryAlgorithm == "EC" else {
55
+ reject("GET_SHARED_KEY_ERROR", "Only ECDH/EC key agreement is supported on iOS", nil)
56
+ return
57
+ }
58
+
59
+ let serverKeyString = (serverPK as String).trimmingCharacters(in: .whitespacesAndNewlines)
60
+ guard !serverKeyString.isEmpty,
61
+ let serverPublicKeyData = Data(base64Encoded: serverKeyString) else {
62
+ reject("GET_SHARED_KEY_ERROR", "Invalid server public key", nil)
63
+ return
64
+ }
34
65
 
35
- sharedKey = try? P256.KeyAgreement.PrivateKey(derRepresentation: privateKeyData).sharedSecretFromKeyAgreement(with: .init(derRepresentation: serverPublicKeyData)).withUnsafeBytes { Data($0).base64EncodedString() }
66
+ let privateKey = try loadOrCreateECDHKeyPair()
67
+ let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(
68
+ with: P256.KeyAgreement.PublicKey(derRepresentation: serverPublicKeyData)
69
+ )
36
70
 
37
- resolve(sharedKey)
71
+ encryptionKeyData = sharedSecret.hkdfDerivedSymmetricKey(
72
+ using: SHA256.self,
73
+ salt: hkdfSalt,
74
+ sharedInfo: hkdfInfoEncryption,
75
+ outputByteCount: 32
76
+ ).withUnsafeBytes { Data($0) }
77
+
78
+ hmacKeyData = sharedSecret.hkdfDerivedSymmetricKey(
79
+ using: SHA256.self,
80
+ salt: hkdfSalt,
81
+ sharedInfo: hkdfInfoHmac,
82
+ outputByteCount: 32
83
+ ).withUnsafeBytes { Data($0) }
84
+
85
+ resolve(nil)
38
86
  } catch {
39
- reject("error", "GET_SHARED_KEY_ERROR", nil)
87
+ reject("GET_SHARED_KEY_ERROR", error.localizedDescription, error)
40
88
  }
41
89
  }
42
-
43
- @objc(encrypt:withResolver:withRejecter:)
44
- func encrypt(input: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
90
+
91
+ @objc(getSharedKey:withOptions:withResolver:withRejecter:)
92
+ func getSharedKey(
93
+ serverPK: NSString,
94
+ options: NSDictionary?,
95
+ resolve: @escaping RCTPromiseResolveBlock,
96
+ reject: RCTPromiseRejectBlock
97
+ ) {
45
98
  do {
46
- guard let keyData = Data(base64Encoded: sharedKey) else {
47
- reject("error", "DECRYPT_SHARED_KEY_ERROR", nil)
99
+ cryptoConfig = try CryptoConfig.from(dictionary: options)
100
+
101
+ guard cryptoConfig.keyAgreementAlgorithm == "ECDH",
102
+ cryptoConfig.keyFactoryAlgorithm == "EC" else {
103
+ reject("GET_SHARED_KEY_ERROR", "Only ECDH/EC key agreement is supported on iOS", nil)
48
104
  return
49
105
  }
50
- let data = Data((input as String).utf8)
51
- if keyData == nil {
52
- reject("error", "keyData is null", nil)
106
+
107
+ let serverKeyString = (serverPK as String).trimmingCharacters(in: .whitespacesAndNewlines)
108
+ guard !serverKeyString.isEmpty,
109
+ let serverPublicKeyData = Data(base64Encoded: serverKeyString) else {
110
+ reject("GET_SHARED_KEY_ERROR", "Invalid server public key", nil)
53
111
  return
54
112
  }
55
- let key = SymmetricKey(data: keyData)
56
113
 
57
- let output = try? AES.GCM.seal(data, using: key).combined?.base64EncodedString()
114
+ let privateKey = try loadOrCreateECDHKeyPair()
115
+ let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(
116
+ with: P256.KeyAgreement.PublicKey(derRepresentation: serverPublicKeyData)
117
+ )
58
118
 
59
- resolve(output)
119
+ encryptionKeyData = sharedSecret.hkdfDerivedSymmetricKey(
120
+ using: SHA256.self,
121
+ salt: hkdfSalt,
122
+ sharedInfo: hkdfInfoEncryption,
123
+ outputByteCount: 32
124
+ ).withUnsafeBytes { Data($0) }
125
+
126
+ hmacKeyData = sharedSecret.hkdfDerivedSymmetricKey(
127
+ using: SHA256.self,
128
+ salt: hkdfSalt,
129
+ sharedInfo: hkdfInfoHmac,
130
+ outputByteCount: 32
131
+ ).withUnsafeBytes { Data($0) }
132
+
133
+ resolve(encryptionKeyData?.base64EncodedString())
60
134
  } catch {
61
- reject("error", "ENCRYPT_ERROR", nil)
135
+ reject("GET_SHARED_KEY_ERROR", error.localizedDescription, error)
62
136
  }
63
137
  }
64
-
65
- @objc(decrypt:withResolver:withRejecter:)
66
- func decrypt(input: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
138
+
139
+ @objc(encrypt:withOptions:withResolver:withRejecter:)
140
+ func encrypt(
141
+ input: NSString,
142
+ options: NSDictionary?,
143
+ resolve: @escaping RCTPromiseResolveBlock,
144
+ reject: RCTPromiseRejectBlock
145
+ ) {
146
+ guard let keyData = encryptionKeyData else {
147
+ reject("ENCRYPT_ERROR", "Encryption key not established. Call getSharedKey first.", nil)
148
+ return
149
+ }
67
150
  do {
68
- guard let keyData = Data(base64Encoded: sharedKey) else {
69
- reject("error", "DECRYPT_SHARED_KEY_ERROR", nil)
151
+ let config = try cryptoConfig.merged(with: options)
152
+ guard config.cipherTransformation == "AES/GCM/NoPadding",
153
+ config.encryptionKeyAlgorithm == "AES" else {
154
+ reject("ENCRYPT_ERROR", "Only AES/GCM/NoPadding is supported on iOS", nil)
70
155
  return
71
156
  }
72
- guard let data = Data(base64Encoded: "\(input)") else {
73
- reject("error", "INPUT_ERROR", nil)
157
+
158
+ let data = Data((input as String).utf8)
159
+ let key = SymmetricKey(data: keyData)
160
+ guard let output = try AES.GCM.seal(data, using: key).combined?.base64EncodedString() else {
161
+ reject("ENCRYPT_ERROR", "Encryption failed", nil)
74
162
  return
75
163
  }
76
- let key = SymmetricKey(data: keyData)
77
- let output = ( try? AES.GCM.open(.init(combined: data), using: key)).map({String(decoding: $0, as: UTF8.self)})
78
-
79
164
  resolve(output)
80
165
  } catch {
81
- reject("error", "DECRYPT_ERROR", nil)
166
+ reject("ENCRYPT_ERROR", error.localizedDescription, error)
82
167
  }
83
168
  }
84
-
85
- @objc(storageEncrypt:withSecretKey:withHardEncryption:withCallback:)
86
- func storageEncrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) -> Void {
169
+
170
+ @objc(decrypt:withOptions:withResolver:withRejecter:)
171
+ func decrypt(
172
+ input: NSString,
173
+ options: NSDictionary?,
174
+ resolve: @escaping RCTPromiseResolveBlock,
175
+ reject: RCTPromiseRejectBlock
176
+ ) {
177
+ guard let keyData = encryptionKeyData else {
178
+ reject("DECRYPT_ERROR", "Encryption key not established. Call getSharedKey first.", nil)
179
+ return
180
+ }
181
+ guard let data = Data(base64Encoded: input as String) else {
182
+ reject("DECRYPT_ERROR", "Invalid ciphertext", nil)
183
+ return
184
+ }
87
185
  do {
88
- var encryptionKey = getDeviceId();
89
- if secretKey != nil {
90
- encryptionKey = secretKey as String;
186
+ let config = try cryptoConfig.merged(with: options)
187
+ guard config.cipherTransformation == "AES/GCM/NoPadding",
188
+ config.encryptionKeyAlgorithm == "AES" else {
189
+ reject("DECRYPT_ERROR", "Only AES/GCM/NoPadding is supported on iOS", nil)
190
+ return
191
+ }
192
+
193
+ let key = SymmetricKey(data: keyData)
194
+ let output = try AES.GCM.open(AES.GCM.SealedBox(combined: data), using: key)
195
+ resolve(String(decoding: output, as: UTF8.self))
196
+ } catch {
197
+ reject("DECRYPT_ERROR", error.localizedDescription, error)
198
+ }
199
+ }
200
+
201
+ @objc(generateJWS:withResolver:withRejecter:)
202
+ func generateJWS(options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
203
+ guard let secret = options["secret"] as? String,
204
+ !secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
205
+ reject("JWS_ERROR", "JWS secret is required and must be a non-empty string", nil)
206
+ return
207
+ }
208
+
209
+ do {
210
+ let payloadString: String
211
+ if let payload = options["payload"] {
212
+ if payload is NSNull {
213
+ payloadString = ""
214
+ } else if let value = payload as? String {
215
+ payloadString = value
216
+ } else {
217
+ reject("JWS_ERROR", "JWS payload must be normalized to a string before calling native code", nil)
218
+ return
219
+ }
220
+ } else {
221
+ payloadString = ""
91
222
  }
92
- let storageEncryption: StorageEncryption = StorageEncryption()
93
- let encrypted = try? storageEncryption.encrypt(plain: "\(input)", encryptionKey: encryptionKey, hardEncryption: hardEncryption)
223
+
224
+ let algorithm = options["algorithm"] as? String
225
+ let headers = options["headers"] as? [String: Any]
226
+ let detached = options["detached"] as? Bool ?? false
227
+ let jws = try JWSGenerator.generate(
228
+ payloadString: payloadString,
229
+ secret: secret,
230
+ algorithm: algorithm,
231
+ headers: headers,
232
+ detached: detached
233
+ )
234
+ resolve(jws)
235
+ } catch {
236
+ reject("JWS_ERROR", error.localizedDescription, error)
237
+ }
238
+ }
239
+
240
+ @objc(obfuscate:withSecret:withResolver:withRejecter:)
241
+ func obfuscate(input: NSString, secret: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
242
+ do {
243
+ let result = try Obfuscation().obfuscate(plain: input as String, secret: secret as String)
244
+ resolve(result)
245
+ } catch {
246
+ reject("OBFUSCATE_ERROR", error.localizedDescription, error)
247
+ }
248
+ }
249
+
250
+ @objc(deobfuscate:withSecret:withResolver:withRejecter:)
251
+ func deobfuscate(input: NSString, secret: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
252
+ do {
253
+ let result = try Obfuscation().deobfuscate(encoded: input as String, secret: secret as String)
254
+ resolve(result)
255
+ } catch {
256
+ reject("DEOBFUSCATE_ERROR", error.localizedDescription, error)
257
+ }
258
+ }
259
+
260
+ @objc(storageEncrypt:withSecretKey:withHardEncryption:withCallback:)
261
+ func storageEncrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) {
262
+ guard secretKey != nil, !(secretKey as String).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
263
+ callback([NSNull(), "secretKey is required. Device identifiers are not accepted as encryption keys."])
264
+ return
265
+ }
266
+ if hardEncryption {
267
+ callback([NSNull(), "hardEncryption is deprecated. Use SecureStorage for encrypted-at-rest data."])
268
+ return
269
+ }
270
+ do {
271
+ let encrypted = try Obfuscation().obfuscate(plain: input as String, secret: secretKey as String)
94
272
  callback([encrypted, NSNull()])
95
273
  } catch {
96
- callback([NSNull(), "SOFT_DECRYPT_ERROR"])
274
+ callback([NSNull(), error.localizedDescription])
97
275
  }
98
276
  }
99
-
277
+
100
278
  @objc(storageDecrypt:withSecretKey:withHardEncryption:withCallback:)
101
- func storageDecrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) -> Void {
279
+ func storageDecrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) {
280
+ guard secretKey != nil, !(secretKey as String).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
281
+ callback([NSNull(), "secretKey is required. Device identifiers are not accepted as encryption keys."])
282
+ return
283
+ }
284
+ if hardEncryption {
285
+ callback([NSNull(), "hardEncryption is deprecated. Use SecureStorage for encrypted-at-rest data."])
286
+ return
287
+ }
102
288
  do {
103
- var encryptionKey = getDeviceId()
104
- if secretKey != nil {
105
- encryptionKey = secretKey as String;
106
- }
107
- let storageEncryption: StorageEncryption = StorageEncryption()
108
- let decrypted = try? storageEncryption.decrypt(decoded: "\(input)", encryptionKey: encryptionKey, hardEncryption: hardEncryption)
289
+ let decrypted = try Obfuscation().deobfuscate(encoded: input as String, secret: secretKey as String)
109
290
  callback([decrypted, NSNull()])
110
291
  } catch {
111
- callback([NSNull(), "SOFT_DECRYPT_ERROR"])
292
+ callback([NSNull(), error.localizedDescription])
112
293
  }
113
294
  }
114
-
115
- @objc(getDeviceId:)
116
- func getDeviceId(callback: RCTResponseSenderBlock) -> Void {
295
+
296
+ @objc(secureStorageSetItem:withValue:withResolver:withRejecter:)
297
+ func secureStorageSetItem(key: NSString, value: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
298
+ do {
299
+ try SecureStorageNative.shared.setItem(key: key as String, value: value as String)
300
+ resolve(nil)
301
+ } catch {
302
+ reject("SECURE_STORAGE_ERROR", error.localizedDescription, error)
303
+ }
304
+ }
305
+
306
+ @objc(secureStorageGetItem:withResolver:withRejecter:)
307
+ func secureStorageGetItem(key: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
308
+ do {
309
+ resolve(try SecureStorageNative.shared.getItem(key: key as String))
310
+ } catch {
311
+ reject("SECURE_STORAGE_ERROR", error.localizedDescription, error)
312
+ }
313
+ }
314
+
315
+ @objc(secureStorageRemoveItem:withResolver:withRejecter:)
316
+ func secureStorageRemoveItem(key: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
317
+ do {
318
+ try SecureStorageNative.shared.removeItem(key: key as String)
319
+ resolve(nil)
320
+ } catch {
321
+ reject("SECURE_STORAGE_ERROR", error.localizedDescription, error)
322
+ }
323
+ }
324
+
325
+ @objc(secureStorageClear:withRejecter:)
326
+ func secureStorageClear(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
117
327
  do {
118
- callback([getDeviceId(), NSNull()]);
328
+ try SecureStorageNative.shared.clear()
329
+ resolve(nil)
119
330
  } catch {
120
- callback([NSNull(), "GET_DEVICE_ID_ERROR"]);
331
+ reject("SECURE_STORAGE_ERROR", error.localizedDescription, error)
121
332
  }
122
333
  }
123
-
124
- func getDeviceId() -> String {
125
- return UIDevice.current.identifierForVendor!.uuidString.replacingOccurrences(of: "-", with: "", options: [], range: nil)
334
+
335
+ @objc(secureStorageGetAllKeys:withRejecter:)
336
+ func secureStorageGetAllKeys(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
337
+ do {
338
+ resolve(try SecureStorageNative.shared.getAllKeys())
339
+ } catch {
340
+ reject("SECURE_STORAGE_ERROR", error.localizedDescription, error)
341
+ }
126
342
  }
127
-
343
+
128
344
  @objc(fetch:withData:withCallback:)
129
- func fetch(url: NSString, data: NSDictionary, callback: @escaping RCTResponseSenderBlock) -> Void {
345
+ func fetch(url: NSString, data: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
346
+ let urlString = url as String
347
+ guard SSLPinning.isHttpsURL(urlString) else {
348
+ callback([NSNull(), "Only HTTPS URLs are allowed"])
349
+ return
350
+ }
351
+
352
+ let pinningConfig = PinningConfiguration(data: data)
353
+ if let error = pinningConfig.error {
354
+ callback([NSNull(), error])
355
+ return
356
+ }
357
+
358
+ guard let requestUrl = URL(string: urlString) else {
359
+ callback([NSNull(), "Invalid URL"])
360
+ return
361
+ }
362
+
363
+ if pinningConfig.enabled {
364
+ guard let host = requestUrl.host,
365
+ SSLPinning.hostnameMatchesValidDomains(host, validDomains: pinningConfig.validDomains) else {
366
+ callback([NSNull(), "Hostname is not in validDomains"])
367
+ return
368
+ }
369
+ }
370
+
130
371
  let configuration = URLSessionConfiguration.default
131
372
  configuration.httpShouldSetCookies = false
132
373
  configuration.httpCookieAcceptPolicy = .never
133
374
  configuration.networkServiceType = .responsiveData
134
375
  configuration.shouldUseExtendedBackgroundIdleMode = true
135
-
136
- var request = URLRequest(url: URL(string: url as String)!)
137
- request.httpMethod = data["method"] as? String ?? "POST"
376
+
377
+ var request = URLRequest(url: requestUrl)
378
+ request.httpMethod = data["method"] as? String ?? "GET"
138
379
  request.timeoutInterval = data["timeout"] as? TimeInterval ?? 60.0
139
- request.allHTTPHeaderFields = data["headers"] as? [String : String]
140
- // Prepare body
380
+ request.allHTTPHeaderFields = data["headers"] as? [String: String]
381
+
141
382
  if let body = data["body"] {
142
383
  if let bodyString = body as? String {
143
384
  request.httpBody = bodyString.data(using: .utf8)
@@ -145,62 +386,97 @@ class SecuritySuite: NSObject {
145
386
  request.httpBody = try? JSONSerialization.data(withJSONObject: bodyDict, options: [])
146
387
  }
147
388
  }
148
- if data["keyId"] != nil && data["requestId"] != nil {
149
- request
150
- .setValue(
151
- jwsHeader(
152
- payload: request.httpBody ?? .init(),
153
- keyId: data["keyId"] as! String,
154
- requestId: data["requestId"] as! String
155
- ),
156
- forHTTPHeaderField: "X-JWS-Signature"
157
- )
158
- }
159
-
160
- let sslPinning = SSLPinning(url: url, data: data, callback: callback)
161
- let session = URLSession(
162
- configuration: configuration,
163
- delegate: sslPinning,
164
- delegateQueue: .main
165
- )
166
- session.dataTask(with: request).resume()
167
- }
168
-
169
- @objc(deviceHasSecurityRisk:withRejecter:)
170
- func deviceHasSecurityRisk(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
171
- do {
172
- let jailbreakStatus = IOSSecuritySuite.amIJailbrokenWithFailMessage()
173
- resolve(jailbreakStatus.jailbroken)
174
- } catch {
175
- reject("ERROR", nil, nil)
389
+
390
+ var jwsHeaderName = "X-Request-Signature"
391
+ if let jwsOptions = data["jws"] as? [String: Any] {
392
+ guard let secret = jwsOptions["secret"] as? String,
393
+ !secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
394
+ callback([NSNull(), "JWS secret is required and must be a non-empty string"])
395
+ return
396
+ }
397
+ do {
398
+ if let customHeaderName = jwsOptions["headerName"] as? String, !customHeaderName.isEmpty {
399
+ jwsHeaderName = customHeaderName
400
+ }
401
+ let headers = jwsOptions["headers"] as? [String: Any]
402
+ let algorithm = jwsOptions["algorithm"] as? String
403
+ let detached = jwsOptions["detached"] as? Bool ?? false
404
+ let method = data["method"] as? String ?? "GET"
405
+ let payloadString = try JwsFetchPayload.build(
406
+ url: urlString,
407
+ method: method,
408
+ requestBody: request.httpBody,
409
+ jwsOptions: jwsOptions
410
+ )
411
+ let signature = try JWSGenerator.generate(
412
+ payloadString: payloadString,
413
+ secret: secret,
414
+ algorithm: algorithm,
415
+ headers: headers,
416
+ detached: detached
417
+ )
418
+ request.setValue(signature, forHTTPHeaderField: jwsHeaderName)
419
+ } catch {
420
+ callback([NSNull(), error.localizedDescription])
421
+ return
422
+ }
423
+ } else if data["keyId"] != nil && data["requestId"] != nil {
424
+ guard let secret = data["secret"] as? String,
425
+ !secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
426
+ let keyId = data["keyId"] as? String,
427
+ let requestId = data["requestId"] as? String else {
428
+ callback([NSNull(), "JWS secret is required. Pass options.jws.secret or options.secret for legacy signing."])
429
+ return
430
+ }
431
+ do {
432
+ let payloadString = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
433
+ let signature = try JWSGenerator.generate(
434
+ payloadString: payloadString,
435
+ secret: secret,
436
+ algorithm: "HS256",
437
+ headers: ["kid": keyId, "request_id": requestId],
438
+ detached: true
439
+ )
440
+ jwsHeaderName = "X-JWS-Signature"
441
+ request.setValue(signature, forHTTPHeaderField: jwsHeaderName)
442
+ } catch {
443
+ callback([NSNull(), error.localizedDescription])
444
+ return
445
+ }
176
446
  }
177
- }
178
447
 
179
- private func convertHMACToBase64URL(hmac: Data) -> String {
180
- let base64Encoded = hmac.base64EncodedString()
181
- let base64URL = base64Encoded
182
- .replacingOccurrences(of: "+", with: "-")
183
- .replacingOccurrences(of: "/", with: "_")
184
- .trimmingCharacters(in: CharacterSet(charactersIn: "="))
185
-
186
- return base64URL
448
+ let sslPinning = SSLPinning(url: url, data: data, callback: callback)
449
+ let session = URLSession(configuration: configuration, delegate: sslPinning, delegateQueue: .main)
450
+ session.dataTask(with: request).resume()
187
451
  }
188
-
189
- private func jwsHeader(payload: Data, keyId: String, requestId: String) -> String {
190
- do {
191
- return (try? JSONEncoder().encode(JoseHeader(kid: keyId, requestId: requestId)))
192
- .flatMap { data in
193
- data.base64EncodedData().urlsafeBase64
194
- }
195
- .flatMap { (data: Data) -> String? in
196
- let value = data + Data([0x2e] /* "." */) + payload
197
- let signature = HMAC<SHA256>.authenticationCode(for: value, using: SymmetricKey(data: Data(base64Encoded: self.sharedKey)!))
198
- let base64URL = convertHMACToBase64URL(hmac: signature.withUnsafeBytes({ Data($0) }))
199
-
200
- return String(decoding: data, as: UTF8.self) + ".." + base64URL
201
- } ?? ""
202
- } catch {
203
- return "";
452
+
453
+ @objc(getDeviceId:)
454
+ func getDeviceId(callback: RCTResponseSenderBlock) {
455
+ guard let id = UIDevice.current.identifierForVendor?.uuidString else {
456
+ callback([NSNull(), "GET_DEVICE_ID_ERROR"])
457
+ return
204
458
  }
459
+ callback([id.replacingOccurrences(of: "-", with: ""), NSNull()])
460
+ }
461
+
462
+ @objc(runtimeDetect:withRejecter:)
463
+ func runtimeDetect(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
464
+ resolve(RuntimeDetector.detect())
465
+ }
466
+
467
+ @objc(appIntegrityVerify:withRejecter:)
468
+ func appIntegrityVerify(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
469
+ resolve(AppIntegrityChecker.verify())
470
+ }
471
+
472
+ @objc(deviceGetEnvironment:withRejecter:)
473
+ func deviceGetEnvironment(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
474
+ resolve(EmulatorDetector.detect())
475
+ }
476
+
477
+ @objc(deviceHasSecurityRisk:withRejecter:)
478
+ func deviceHasSecurityRisk(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
479
+ let jailbreakStatus = IOSSecuritySuite.amIJailbrokenWithFailMessage()
480
+ resolve(jailbreakStatus.jailbroken)
205
481
  }
206
482
  }