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
|
@@ -31,8 +31,6 @@ import java.util.HashMap;
|
|
|
31
31
|
import java.util.Map;
|
|
32
32
|
import java.util.concurrent.TimeUnit;
|
|
33
33
|
|
|
34
|
-
import javax.crypto.SecretKey;
|
|
35
|
-
|
|
36
34
|
import okhttp3.CertificatePinner;
|
|
37
35
|
import okhttp3.Headers;
|
|
38
36
|
import okhttp3.MediaType;
|
|
@@ -45,10 +43,10 @@ import okhttp3.Response;
|
|
|
45
43
|
import okio.Buffer;
|
|
46
44
|
|
|
47
45
|
public class Sslpinning {
|
|
48
|
-
private ReactApplicationContext context;
|
|
46
|
+
private final ReactApplicationContext context;
|
|
49
47
|
private static String hostname = "";
|
|
50
|
-
private static String
|
|
51
|
-
public static MediaType mediaType = MediaType.parse(
|
|
48
|
+
private static final String CONTENT_TYPE = "application/json; charset=utf-8";
|
|
49
|
+
public static final MediaType mediaType = MediaType.parse(CONTENT_TYPE);
|
|
52
50
|
String responseBodyString = "{}";
|
|
53
51
|
Callback callback;
|
|
54
52
|
|
|
@@ -56,22 +54,34 @@ public class Sslpinning {
|
|
|
56
54
|
this.context = context;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
public void fetch(String url, final ReadableMap options,
|
|
57
|
+
public void fetch(String url, final ReadableMap options, Callback callback) {
|
|
60
58
|
this.callback = callback;
|
|
61
59
|
|
|
62
|
-
if (!
|
|
63
|
-
callback.invoke(null, "
|
|
60
|
+
if (!CryptoUtils.isHttpsUrl(url)) {
|
|
61
|
+
callback.invoke(null, "Only HTTPS URLs are allowed");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
PinningConfig pinningConfig = PinningConfig.fromOptions(options);
|
|
66
|
+
if (pinningConfig.hasError()) {
|
|
67
|
+
callback.invoke(null, pinningConfig.getError());
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
try {
|
|
68
72
|
this.hostname = getHostname(url);
|
|
69
73
|
} catch (URISyntaxException e) {
|
|
70
|
-
|
|
74
|
+
callback.invoke(null, "Invalid URL hostname");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (pinningConfig.isEnabled() && !isValidDomain(this.hostname, options)) {
|
|
79
|
+
callback.invoke(null, "Hostname '" + this.hostname + "' is not in validDomains");
|
|
80
|
+
return;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
try {
|
|
74
|
-
OkHttpClient client = getClient(options);
|
|
84
|
+
OkHttpClient client = getClient(options, pinningConfig);
|
|
75
85
|
|
|
76
86
|
Headers header = setHeader(options);
|
|
77
87
|
String method = getMethod(options);
|
|
@@ -82,34 +92,68 @@ public class Sslpinning {
|
|
|
82
92
|
return;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
// JWS handler
|
|
86
95
|
String jwsHeader = "";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
String jwsHeaderName = "X-Request-Signature";
|
|
97
|
+
if (options.hasKey("jws") && options.getMap("jws") != null) {
|
|
98
|
+
ReadableMap jwsOptions = options.getMap("jws");
|
|
99
|
+
if (jwsOptions == null || !jwsOptions.hasKey("secret")) {
|
|
100
|
+
callback.invoke(null, "JWS secret is required and must be a non-empty string");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
String secret = jwsOptions.getString("secret");
|
|
104
|
+
if (secret == null || secret.trim().isEmpty()) {
|
|
105
|
+
callback.invoke(null, "JWS secret is required and must be a non-empty string");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Buffer buffer = new Buffer();
|
|
94
|
-
try {
|
|
95
|
-
requestBody.writeTo(buffer);
|
|
96
|
-
payload = buffer.readByteArray();
|
|
97
|
-
} catch (IOException e) {
|
|
98
|
-
e.printStackTrace();
|
|
99
|
-
}
|
|
109
|
+
if (jwsOptions.hasKey("headerName") && jwsOptions.getString("headerName") != null) {
|
|
110
|
+
jwsHeaderName = jwsOptions.getString("headerName");
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
byte[] requestBodyBytes = extractPayload(requestBody);
|
|
114
|
+
String payloadString = JwsFetchPayload.build(url, method, requestBodyBytes, jwsOptions);
|
|
115
|
+
boolean detached = jwsOptions.hasKey("detached") && jwsOptions.getBoolean("detached");
|
|
116
|
+
String algorithm = jwsOptions.hasKey("algorithm") ? jwsOptions.getString("algorithm") : null;
|
|
117
|
+
ReadableMap headers = jwsOptions.hasKey("headers") ? jwsOptions.getMap("headers") : null;
|
|
118
|
+
|
|
102
119
|
JWSGenerator jwsGenerator = new JWSGenerator();
|
|
103
|
-
jwsHeader = jwsGenerator.
|
|
120
|
+
jwsHeader = jwsGenerator.generate(
|
|
121
|
+
payloadString,
|
|
122
|
+
secret,
|
|
123
|
+
algorithm,
|
|
124
|
+
headers,
|
|
125
|
+
detached
|
|
126
|
+
);
|
|
127
|
+
} else if (options.hasKey("keyId") && options.hasKey("requestId")) {
|
|
128
|
+
if (!options.hasKey("secret")) {
|
|
129
|
+
callback.invoke(null, "JWS secret is required. Pass options.jws.secret or options.secret for legacy signing.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
String secret = options.getString("secret");
|
|
133
|
+
if (secret == null || secret.trim().isEmpty()) {
|
|
134
|
+
callback.invoke(null, "JWS secret is required and must be a non-empty string");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
String keyId = options.getString("keyId");
|
|
138
|
+
String requestId = options.getString("requestId");
|
|
139
|
+
byte[] payload = extractPayload(requestBody);
|
|
140
|
+
JWSGenerator jwsGenerator = new JWSGenerator();
|
|
141
|
+
jwsHeader = jwsGenerator.jwsHeader(payload, keyId, requestId, secret);
|
|
142
|
+
jwsHeaderName = "X-JWS-Signature";
|
|
104
143
|
}
|
|
105
144
|
|
|
106
|
-
Request
|
|
145
|
+
Request.Builder requestBuilder = new Request.Builder()
|
|
107
146
|
.url(url)
|
|
108
|
-
.
|
|
109
|
-
.addHeader("X-JWS-Signature", jwsHeader)
|
|
110
|
-
.method(method, requestBody)
|
|
111
|
-
.build();
|
|
147
|
+
.method(method, requestBody);
|
|
112
148
|
|
|
149
|
+
if (header != null) {
|
|
150
|
+
requestBuilder.headers(header);
|
|
151
|
+
}
|
|
152
|
+
if (!jwsHeader.isEmpty()) {
|
|
153
|
+
requestBuilder.addHeader(jwsHeaderName, jwsHeader);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Request request = requestBuilder.build();
|
|
113
157
|
WritableMap output = Arguments.createMap();
|
|
114
158
|
|
|
115
159
|
try {
|
|
@@ -122,7 +166,6 @@ public class Sslpinning {
|
|
|
122
166
|
output.putInt("status", responseCode);
|
|
123
167
|
output.putString("url", request.url().toString());
|
|
124
168
|
|
|
125
|
-
// response time
|
|
126
169
|
long tx = response.sentRequestAtMillis();
|
|
127
170
|
long rx = response.receivedResponseAtMillis();
|
|
128
171
|
output.putString("duration", (rx - tx) + "ms");
|
|
@@ -135,37 +178,48 @@ public class Sslpinning {
|
|
|
135
178
|
output.putString("response", responseBodyString);
|
|
136
179
|
callback.invoke(output, null);
|
|
137
180
|
} catch (IOException e) {
|
|
138
|
-
output.putString("error", responseBodyString);
|
|
139
|
-
|
|
181
|
+
output.putString("error", e.getMessage() != null ? e.getMessage() : responseBodyString);
|
|
140
182
|
callback.invoke(null, output);
|
|
141
|
-
if (e instanceof SocketTimeoutException) {
|
|
142
|
-
System.err.print("Socket TimeOut");
|
|
143
|
-
} else {
|
|
183
|
+
if (!(e instanceof SocketTimeoutException)) {
|
|
144
184
|
e.printStackTrace();
|
|
145
185
|
}
|
|
146
186
|
}
|
|
147
|
-
} catch (
|
|
148
|
-
callback.invoke(null, e);
|
|
187
|
+
} catch (Exception e) {
|
|
188
|
+
callback.invoke(null, e.getMessage() != null ? e.getMessage() : "Request failed");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private byte[] extractPayload(RequestBody requestBody) throws IOException {
|
|
193
|
+
if (requestBody == null) {
|
|
194
|
+
return new byte[0];
|
|
149
195
|
}
|
|
196
|
+
Buffer buffer = new Buffer();
|
|
197
|
+
requestBody.writeTo(buffer);
|
|
198
|
+
return buffer.readByteArray();
|
|
150
199
|
}
|
|
151
200
|
|
|
152
201
|
private CertificatePinner getCertificatePinner(ReadableMap options) {
|
|
153
202
|
CertificatePinner.Builder certificatePinner = new CertificatePinner.Builder();
|
|
154
|
-
|
|
155
203
|
ReadableArray hashes = options.getArray("certificates");
|
|
204
|
+
|
|
156
205
|
for (int i = 0; i < hashes.size(); i++) {
|
|
157
|
-
|
|
206
|
+
String pin = CryptoUtils.normalizePinHash(hashes.getString(i));
|
|
207
|
+
if (pin.isEmpty()) {
|
|
208
|
+
throw new IllegalArgumentException("Invalid certificate/public key pin at index " + i);
|
|
209
|
+
}
|
|
210
|
+
// OkHttp CertificatePinner pins SPKI SHA-256 hashes (public key pinning).
|
|
211
|
+
certificatePinner.add(this.hostname, "sha256/" + pin);
|
|
158
212
|
}
|
|
159
213
|
|
|
160
214
|
return certificatePinner.build();
|
|
161
215
|
}
|
|
162
216
|
|
|
163
|
-
private OkHttpClient getClient(ReadableMap options
|
|
217
|
+
private OkHttpClient getClient(ReadableMap options, PinningConfig pinningConfig) {
|
|
164
218
|
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
|
165
219
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
builder.certificatePinner(
|
|
220
|
+
// Fail-closed: once pinning is configured, OkHttp enforces pins with no system-trust fallback.
|
|
221
|
+
if (pinningConfig.isEnabled()) {
|
|
222
|
+
builder.certificatePinner(getCertificatePinner(options));
|
|
169
223
|
}
|
|
170
224
|
|
|
171
225
|
if (options.hasKey("timeout")) {
|
|
@@ -175,25 +229,34 @@ public class Sslpinning {
|
|
|
175
229
|
.writeTimeout(timeout, TimeUnit.MILLISECONDS);
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
|
|
179
|
-
|
|
232
|
+
// Hard-gate network logging to debug builds only.
|
|
233
|
+
if (options.hasKey("loggerIsEnabled")
|
|
234
|
+
&& options.getBoolean("loggerIsEnabled")
|
|
235
|
+
&& BuildConfig.DEBUG) {
|
|
236
|
+
ChuckerInterceptor.Builder chuckerBuilder = new ChuckerInterceptor.Builder(context);
|
|
237
|
+
for (String header : HeaderSanitizer.SENSITIVE_HEADERS) {
|
|
238
|
+
chuckerBuilder.redactHeader(header);
|
|
239
|
+
}
|
|
240
|
+
builder.addInterceptor(chuckerBuilder.build());
|
|
180
241
|
}
|
|
181
242
|
|
|
182
243
|
return builder.build();
|
|
183
244
|
}
|
|
184
245
|
|
|
185
246
|
private static String getHostname(String url) throws URISyntaxException {
|
|
186
|
-
URI uri = new URI(url);
|
|
247
|
+
URI uri = new URI(url.trim());
|
|
187
248
|
String domain = uri.getHost();
|
|
249
|
+
if (domain == null) {
|
|
250
|
+
throw new URISyntaxException(url, "Missing host");
|
|
251
|
+
}
|
|
188
252
|
return domain.startsWith("www.") ? domain.substring(4) : domain;
|
|
189
253
|
}
|
|
190
254
|
|
|
191
255
|
private String getMethod(ReadableMap options) {
|
|
192
256
|
String method = "GET";
|
|
193
|
-
if (options.hasKey("method")) {
|
|
194
|
-
method = options.getString("method");
|
|
257
|
+
if (options.hasKey("method") && options.getString("method") != null) {
|
|
258
|
+
method = options.getString("method").toUpperCase();
|
|
195
259
|
}
|
|
196
|
-
|
|
197
260
|
return method;
|
|
198
261
|
}
|
|
199
262
|
|
|
@@ -204,38 +267,26 @@ public class Sslpinning {
|
|
|
204
267
|
|
|
205
268
|
ReadableMap headers = options.getMap("headers");
|
|
206
269
|
Headers.Builder builder = new Headers.Builder();
|
|
207
|
-
|
|
208
270
|
Map<String, String> headersMap = readableMapToHashMap(headers);
|
|
209
271
|
for (Map.Entry<String, String> set : headersMap.entrySet()) {
|
|
210
272
|
builder.add(set.getKey(), set.getValue());
|
|
211
273
|
}
|
|
212
|
-
|
|
213
274
|
return builder.build();
|
|
214
275
|
}
|
|
215
276
|
|
|
216
|
-
private HashMap readableMapToHashMap(ReadableMap readableMap) {
|
|
277
|
+
private HashMap<String, String> readableMapToHashMap(ReadableMap readableMap) {
|
|
278
|
+
HashMap<String, String> map = new HashMap<>();
|
|
217
279
|
if (readableMap == null) {
|
|
218
|
-
return
|
|
280
|
+
return map;
|
|
219
281
|
}
|
|
220
282
|
|
|
221
|
-
HashMap map = new HashMap<String, String>();
|
|
222
283
|
ReadableMapKeySetIterator keySetIterator = readableMap.keySetIterator();
|
|
223
284
|
while (keySetIterator.hasNextKey()) {
|
|
224
285
|
String key = keySetIterator.nextKey();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
case String:
|
|
228
|
-
map.put(key, readableMap.getString(key));
|
|
229
|
-
break;
|
|
230
|
-
case Map:
|
|
231
|
-
HashMap<String, Object> attributes = this.readableMapToHashMap(readableMap.getMap(key));
|
|
232
|
-
map.put(key, attributes);
|
|
233
|
-
break;
|
|
234
|
-
default:
|
|
235
|
-
// do nothing
|
|
286
|
+
if (readableMap.getType(key) == ReadableType.String) {
|
|
287
|
+
map.put(key, readableMap.getString(key));
|
|
236
288
|
}
|
|
237
289
|
}
|
|
238
|
-
|
|
239
290
|
return map;
|
|
240
291
|
}
|
|
241
292
|
|
|
@@ -248,36 +299,31 @@ public class Sslpinning {
|
|
|
248
299
|
}
|
|
249
300
|
|
|
250
301
|
private RequestBody setBody(ReadableMap options) {
|
|
251
|
-
RequestBody body = null;
|
|
252
|
-
|
|
253
302
|
if (!options.hasKey("body")) {
|
|
254
|
-
return
|
|
303
|
+
return null;
|
|
255
304
|
}
|
|
256
305
|
|
|
257
306
|
ReadableType bodyType = options.getType("body");
|
|
258
307
|
switch (bodyType) {
|
|
259
308
|
case String:
|
|
260
|
-
|
|
261
|
-
break;
|
|
309
|
+
return RequestBody.create(mediaType, options.getString("body"));
|
|
262
310
|
case Map:
|
|
263
311
|
ReadableMap bodyMap = options.getMap("body");
|
|
264
312
|
if (bodyMap.hasKey("formData")) {
|
|
265
|
-
|
|
266
|
-
body = getBody(formData);
|
|
313
|
+
return getBody(bodyMap.getMap("formData"));
|
|
267
314
|
} else if (bodyMap.hasKey("_parts")) {
|
|
268
|
-
|
|
315
|
+
return getBody(bodyMap);
|
|
269
316
|
} else {
|
|
270
|
-
|
|
317
|
+
return RequestBody.create(mediaType, bodyMap.toString());
|
|
271
318
|
}
|
|
272
|
-
|
|
319
|
+
default:
|
|
320
|
+
return null;
|
|
273
321
|
}
|
|
274
|
-
|
|
275
|
-
return body;
|
|
276
322
|
}
|
|
277
323
|
|
|
278
324
|
private RequestBody getBody(ReadableMap body) {
|
|
279
|
-
MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
|
|
280
|
-
|
|
325
|
+
MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
|
|
326
|
+
.setType(MultipartBody.FORM);
|
|
281
327
|
if (body.hasKey("_parts")) {
|
|
282
328
|
ReadableArray parts = body.getArray("_parts");
|
|
283
329
|
for (int i = 0; i < parts.size(); i++) {
|
|
@@ -293,53 +339,120 @@ public class Sslpinning {
|
|
|
293
339
|
ReadableMap fileData = part.getMap(1);
|
|
294
340
|
addFormDataPart(this.context, multipartBodyBuilder, fileData, key);
|
|
295
341
|
} else {
|
|
296
|
-
|
|
297
|
-
multipartBodyBuilder.addFormDataPart(key, value);
|
|
342
|
+
multipartBodyBuilder.addFormDataPart(key, part.getString(1));
|
|
298
343
|
}
|
|
299
344
|
}
|
|
300
345
|
}
|
|
301
346
|
return multipartBodyBuilder.build();
|
|
302
347
|
}
|
|
303
348
|
|
|
304
|
-
private static void addFormDataPart(
|
|
305
|
-
|
|
306
|
-
|
|
349
|
+
private static void addFormDataPart(
|
|
350
|
+
Context context,
|
|
351
|
+
MultipartBody.Builder multipartBodyBuilder,
|
|
352
|
+
ReadableMap fileData,
|
|
353
|
+
String key
|
|
354
|
+
) {
|
|
355
|
+
Uri uri = Uri.parse("");
|
|
307
356
|
if (fileData.hasKey("uri")) {
|
|
308
|
-
|
|
357
|
+
uri = Uri.parse(fileData.getString("uri"));
|
|
309
358
|
} else if (fileData.hasKey("path")) {
|
|
310
|
-
|
|
359
|
+
uri = Uri.parse(fileData.getString("path"));
|
|
311
360
|
}
|
|
312
361
|
String type = fileData.getString("type");
|
|
313
|
-
String fileName = ""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
} else if (fileData.hasKey("name")) {
|
|
317
|
-
fileName = fileData.getString("name");
|
|
318
|
-
}
|
|
362
|
+
String fileName = fileData.hasKey("fileName")
|
|
363
|
+
? fileData.getString("fileName")
|
|
364
|
+
: fileData.getString("name");
|
|
319
365
|
|
|
320
366
|
try {
|
|
321
|
-
File file = getTempFile(context,
|
|
367
|
+
File file = getTempFile(context, uri);
|
|
322
368
|
multipartBodyBuilder.addFormDataPart(key, fileName, RequestBody.create(MediaType.parse(type), file));
|
|
323
369
|
} catch (IOException e) {
|
|
324
|
-
e
|
|
370
|
+
throw new IllegalStateException("Failed to read multipart file", e);
|
|
325
371
|
}
|
|
326
372
|
}
|
|
327
373
|
|
|
328
374
|
public static File getTempFile(Context context, Uri uri) throws IOException {
|
|
329
375
|
File file = File.createTempFile("media", null);
|
|
330
|
-
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
376
|
+
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
|
377
|
+
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) {
|
|
378
|
+
if (inputStream == null) {
|
|
379
|
+
throw new IOException("Unable to open URI: " + uri);
|
|
380
|
+
}
|
|
381
|
+
byte[] buffer = new byte[1024];
|
|
382
|
+
int len;
|
|
383
|
+
while ((len = inputStream.read(buffer)) != -1) {
|
|
384
|
+
outputStream.write(buffer, 0, len);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
338
387
|
return file;
|
|
339
388
|
}
|
|
340
389
|
|
|
341
|
-
private boolean
|
|
342
|
-
|
|
343
|
-
|
|
390
|
+
private boolean isValidDomain(String hostname, ReadableMap options) {
|
|
391
|
+
ReadableArray domains = options.getArray("validDomains");
|
|
392
|
+
if (domains == null || domains.size() == 0) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (int i = 0; i < domains.size(); i++) {
|
|
397
|
+
String domain = domains.getString(i);
|
|
398
|
+
if (domain == null || domain.trim().isEmpty()) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
String normalized = domain.trim().toLowerCase();
|
|
402
|
+
String host = hostname.toLowerCase();
|
|
403
|
+
if (host.equals(normalized) || host.endsWith("." + normalized)) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
static final class PinningConfig {
|
|
411
|
+
private final boolean enabled;
|
|
412
|
+
private final String error;
|
|
413
|
+
|
|
414
|
+
private PinningConfig(boolean enabled, String error) {
|
|
415
|
+
this.enabled = enabled;
|
|
416
|
+
this.error = error;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static PinningConfig fromOptions(ReadableMap options) {
|
|
420
|
+
boolean hasCertificates = options.hasKey("certificates");
|
|
421
|
+
boolean hasValidDomains = options.hasKey("validDomains");
|
|
422
|
+
|
|
423
|
+
if (hasCertificates != hasValidDomains) {
|
|
424
|
+
return new PinningConfig(
|
|
425
|
+
false,
|
|
426
|
+
"SSL pinning requires both 'certificates' (SPKI SHA-256 hashes) and 'validDomains'"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!hasCertificates) {
|
|
431
|
+
return new PinningConfig(false, null);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
ReadableArray certs = options.getArray("certificates");
|
|
435
|
+
ReadableArray domains = options.getArray("validDomains");
|
|
436
|
+
if (certs == null || certs.size() == 0) {
|
|
437
|
+
return new PinningConfig(false, "At least one certificate/public key pin is required");
|
|
438
|
+
}
|
|
439
|
+
if (domains == null || domains.size() == 0) {
|
|
440
|
+
return new PinningConfig(false, "At least one valid domain is required");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return new PinningConfig(true, null);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
boolean isEnabled() {
|
|
447
|
+
return enabled;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
boolean hasError() {
|
|
451
|
+
return error != null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
String getError() {
|
|
455
|
+
return error;
|
|
456
|
+
}
|
|
344
457
|
}
|
|
345
458
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.securitysuite.security;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.pm.ApplicationInfo;
|
|
5
|
+
import android.content.pm.PackageInfo;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.content.pm.Signature;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
|
|
10
|
+
import com.facebook.react.bridge.Arguments;
|
|
11
|
+
import com.facebook.react.bridge.WritableMap;
|
|
12
|
+
|
|
13
|
+
import java.security.MessageDigest;
|
|
14
|
+
import java.util.Arrays;
|
|
15
|
+
import java.util.HashSet;
|
|
16
|
+
import java.util.Locale;
|
|
17
|
+
import java.util.Set;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Native app integrity checks (Android).
|
|
21
|
+
*/
|
|
22
|
+
public final class AppIntegrityChecker {
|
|
23
|
+
private static final Set<String> TRUSTED_INSTALLERS = new HashSet<>(Arrays.asList(
|
|
24
|
+
"com.android.vending",
|
|
25
|
+
"com.google.android.packageinstaller",
|
|
26
|
+
"com.android.packageinstaller"
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
private AppIntegrityChecker() {}
|
|
30
|
+
|
|
31
|
+
public static WritableMap verify(Context context) {
|
|
32
|
+
WritableMap result = Arguments.createMap();
|
|
33
|
+
PackageManager pm = context.getPackageManager();
|
|
34
|
+
String packageName = context.getPackageName();
|
|
35
|
+
|
|
36
|
+
boolean debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
|
37
|
+
String buildType = debuggable ? "debug" : "release";
|
|
38
|
+
String signingSha256 = getSigningCertificateSha256(pm, packageName);
|
|
39
|
+
boolean validSignature = signingSha256 != null && !signingSha256.isEmpty();
|
|
40
|
+
|
|
41
|
+
String installer = getInstallerPackageName(pm, packageName);
|
|
42
|
+
boolean installerTrusted = installer == null
|
|
43
|
+
? debuggable
|
|
44
|
+
: TRUSTED_INSTALLERS.contains(installer);
|
|
45
|
+
|
|
46
|
+
boolean tampered = !validSignature
|
|
47
|
+
|| (!debuggable && installer != null && !installerTrusted)
|
|
48
|
+
|| hasSuspiciousSplitSource(context.getApplicationInfo());
|
|
49
|
+
|
|
50
|
+
result.putBoolean("validSignature", validSignature);
|
|
51
|
+
result.putBoolean("debuggable", debuggable);
|
|
52
|
+
result.putBoolean("tampered", tampered);
|
|
53
|
+
result.putBoolean("installerTrusted", installerTrusted);
|
|
54
|
+
result.putString("buildType", buildType);
|
|
55
|
+
if (signingSha256 != null) {
|
|
56
|
+
result.putString("signingCertificateSha256", signingSha256);
|
|
57
|
+
}
|
|
58
|
+
if (installer != null) {
|
|
59
|
+
result.putString("installerPackage", installer);
|
|
60
|
+
} else {
|
|
61
|
+
result.putNull("installerPackage");
|
|
62
|
+
}
|
|
63
|
+
result.putString("bundleIdentifier", packageName);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static String getSigningCertificateSha256(PackageManager pm, String packageName) {
|
|
68
|
+
try {
|
|
69
|
+
Signature[] signatures;
|
|
70
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
71
|
+
PackageInfo info = pm.getPackageInfo(
|
|
72
|
+
packageName,
|
|
73
|
+
PackageManager.GET_SIGNING_CERTIFICATES
|
|
74
|
+
);
|
|
75
|
+
if (info.signingInfo == null) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
signatures = info.signingInfo.hasMultipleSigners()
|
|
79
|
+
? info.signingInfo.getApkContentsSigners()
|
|
80
|
+
: info.signingInfo.getSigningCertificateHistory();
|
|
81
|
+
} else {
|
|
82
|
+
@SuppressWarnings("deprecation")
|
|
83
|
+
PackageInfo info = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
|
|
84
|
+
@SuppressWarnings("deprecation")
|
|
85
|
+
Signature[] legacySignatures = info.signatures;
|
|
86
|
+
signatures = legacySignatures;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (signatures == null || signatures.length == 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
94
|
+
byte[] hash = digest.digest(signatures[0].toByteArray());
|
|
95
|
+
return bytesToHex(hash).toLowerCase(Locale.US);
|
|
96
|
+
} catch (Exception ignored) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static String getInstallerPackageName(PackageManager pm, String packageName) {
|
|
102
|
+
try {
|
|
103
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
104
|
+
return pm.getInstallSourceInfo(packageName).getInstallingPackageName();
|
|
105
|
+
}
|
|
106
|
+
@SuppressWarnings("deprecation")
|
|
107
|
+
String installer = pm.getInstallerPackageName(packageName);
|
|
108
|
+
return installer;
|
|
109
|
+
} catch (Exception ignored) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private static boolean hasSuspiciousSplitSource(ApplicationInfo info) {
|
|
115
|
+
if (info.splitSourceDirs == null) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
for (String split : info.splitSourceDirs) {
|
|
119
|
+
if (split != null && split.toLowerCase(Locale.US).contains("/data/local/tmp")) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static String bytesToHex(byte[] bytes) {
|
|
127
|
+
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
|
128
|
+
for (byte value : bytes) {
|
|
129
|
+
builder.append(String.format(Locale.US, "%02x", value));
|
|
130
|
+
}
|
|
131
|
+
return builder.toString();
|
|
132
|
+
}
|
|
133
|
+
}
|