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,152 @@
|
|
|
1
|
+
package com.securitysuite;
|
|
2
|
+
|
|
3
|
+
import android.util.Base64;
|
|
4
|
+
|
|
5
|
+
import javax.crypto.Mac;
|
|
6
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
7
|
+
|
|
8
|
+
import java.nio.charset.StandardCharsets;
|
|
9
|
+
import java.security.MessageDigest;
|
|
10
|
+
import java.security.SecureRandom;
|
|
11
|
+
import java.util.regex.Pattern;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared cryptographic utilities. HKDF-SHA256 (RFC 5869) is used instead of raw
|
|
15
|
+
* ECDH output or single-pass SHA-256 to derive independent symmetric keys.
|
|
16
|
+
*/
|
|
17
|
+
public final class CryptoUtils {
|
|
18
|
+
public static final String HKDF_SALT = "react-native-security-suite";
|
|
19
|
+
public static final String HKDF_INFO_ENCRYPTION = "rss-encryption-v1";
|
|
20
|
+
public static final String HKDF_INFO_HMAC = "rss-hmac-v1";
|
|
21
|
+
|
|
22
|
+
private static final Pattern SAFE_JWS_HEADER_KEY = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_-]*$");
|
|
23
|
+
private static final Pattern SAFE_JWS_VALUE = Pattern.compile("^[\\x20-\\x7E]+$");
|
|
24
|
+
|
|
25
|
+
private CryptoUtils() {}
|
|
26
|
+
|
|
27
|
+
/** RFC 5869 HKDF-SHA256 expand/extract. */
|
|
28
|
+
public static byte[] hkdfSha256(byte[] ikm, byte[] salt, byte[] info, int length) throws Exception {
|
|
29
|
+
byte[] actualSalt = (salt == null || salt.length == 0)
|
|
30
|
+
? new byte[32]
|
|
31
|
+
: salt;
|
|
32
|
+
|
|
33
|
+
Mac extractMac = Mac.getInstance("HmacSHA256");
|
|
34
|
+
extractMac.init(new SecretKeySpec(actualSalt, "HmacSHA256"));
|
|
35
|
+
byte[] prk = extractMac.doFinal(ikm);
|
|
36
|
+
|
|
37
|
+
Mac expandMac = Mac.getInstance("HmacSHA256");
|
|
38
|
+
expandMac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
|
39
|
+
|
|
40
|
+
byte[] result = new byte[length];
|
|
41
|
+
byte[] previous = new byte[0];
|
|
42
|
+
int offset = 0;
|
|
43
|
+
byte counter = 1;
|
|
44
|
+
|
|
45
|
+
while (offset < length) {
|
|
46
|
+
expandMac.reset();
|
|
47
|
+
expandMac.update(previous);
|
|
48
|
+
if (info != null && info.length > 0) {
|
|
49
|
+
expandMac.update(info);
|
|
50
|
+
}
|
|
51
|
+
expandMac.update(counter);
|
|
52
|
+
previous = expandMac.doFinal();
|
|
53
|
+
int copyLength = Math.min(previous.length, length - offset);
|
|
54
|
+
System.arraycopy(previous, 0, result, offset, copyLength);
|
|
55
|
+
offset += copyLength;
|
|
56
|
+
counter++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static byte[] deriveEncryptionKey(byte[] sharedSecret) throws Exception {
|
|
63
|
+
return hkdfSha256(
|
|
64
|
+
sharedSecret,
|
|
65
|
+
HKDF_SALT.getBytes(StandardCharsets.UTF_8),
|
|
66
|
+
HKDF_INFO_ENCRYPTION.getBytes(StandardCharsets.UTF_8),
|
|
67
|
+
32
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static byte[] deriveHmacKey(byte[] sharedSecret) throws Exception {
|
|
72
|
+
return hkdfSha256(
|
|
73
|
+
sharedSecret,
|
|
74
|
+
HKDF_SALT.getBytes(StandardCharsets.UTF_8),
|
|
75
|
+
HKDF_INFO_HMAC.getBytes(StandardCharsets.UTF_8),
|
|
76
|
+
32
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public static String base64UrlEncode(byte[] data) {
|
|
81
|
+
return Base64.encodeToString(data, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public static boolean isHttpsUrl(String url) {
|
|
85
|
+
if (url == null || url.isEmpty()) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
java.net.URI uri = new java.net.URI(url.trim());
|
|
90
|
+
return "https".equalsIgnoreCase(uri.getScheme())
|
|
91
|
+
&& uri.getHost() != null
|
|
92
|
+
&& !uri.getHost().isEmpty();
|
|
93
|
+
} catch (Exception e) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public static String normalizePinHash(String pin) {
|
|
99
|
+
if (pin == null) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
return pin.trim().replaceAll("(?i)^sha256/", "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public static void validateJwsHeaderKey(String key) throws IllegalArgumentException {
|
|
106
|
+
if (key == null || !SAFE_JWS_HEADER_KEY.matcher(key).matches()) {
|
|
107
|
+
throw new IllegalArgumentException("Invalid JWS header key: " + key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public static void validateJwsHeaderValue(String value) throws IllegalArgumentException {
|
|
112
|
+
if (value == null || !SAFE_JWS_VALUE.matcher(value).matches()) {
|
|
113
|
+
throw new IllegalArgumentException("Invalid JWS header value");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public static String validateJwsAlgorithm(String algorithm) throws IllegalArgumentException {
|
|
118
|
+
if (algorithm == null || algorithm.isEmpty()) {
|
|
119
|
+
return "HS256";
|
|
120
|
+
}
|
|
121
|
+
switch (algorithm) {
|
|
122
|
+
case "HS256":
|
|
123
|
+
case "HS384":
|
|
124
|
+
case "HS512":
|
|
125
|
+
return algorithm;
|
|
126
|
+
default:
|
|
127
|
+
throw new IllegalArgumentException("Unsupported JWS algorithm: " + algorithm);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public static String hmacAlgorithmForJws(String algorithm) {
|
|
132
|
+
switch (algorithm) {
|
|
133
|
+
case "HS384":
|
|
134
|
+
return "HmacSHA384";
|
|
135
|
+
case "HS512":
|
|
136
|
+
return "HmacSHA512";
|
|
137
|
+
case "HS256":
|
|
138
|
+
default:
|
|
139
|
+
return "HmacSHA256";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public static byte[] sha256(byte[] input) throws Exception {
|
|
144
|
+
return MessageDigest.getInstance("SHA-256").digest(input);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public static byte[] randomBytes(int length) {
|
|
148
|
+
byte[] bytes = new byte[length];
|
|
149
|
+
new SecureRandom().nextBytes(bytes);
|
|
150
|
+
return bytes;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.securitysuite;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.security.keystore.KeyGenParameterSpec;
|
|
5
|
+
import android.security.keystore.KeyProperties;
|
|
6
|
+
import android.util.Base64;
|
|
7
|
+
|
|
8
|
+
import java.security.KeyPair;
|
|
9
|
+
import java.security.KeyPairGenerator;
|
|
10
|
+
import java.security.KeyStore;
|
|
11
|
+
import java.security.PrivateKey;
|
|
12
|
+
import java.security.PublicKey;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persists the ECDH P-256 keypair in Android Keystore (non-exportable private key).
|
|
16
|
+
*/
|
|
17
|
+
public final class EcdhKeyStore {
|
|
18
|
+
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
|
|
19
|
+
private static final String KEY_ALIAS = "com.securitysuite.ecdh.p256";
|
|
20
|
+
|
|
21
|
+
private EcdhKeyStore() {}
|
|
22
|
+
|
|
23
|
+
public static KeyPair getOrCreateKeyPair(Context context) throws Exception {
|
|
24
|
+
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
|
|
25
|
+
keyStore.load(null);
|
|
26
|
+
|
|
27
|
+
if (keyStore.containsAlias(KEY_ALIAS)) {
|
|
28
|
+
KeyStore.Entry entry = keyStore.getEntry(KEY_ALIAS, null);
|
|
29
|
+
if (entry instanceof KeyStore.PrivateKeyEntry) {
|
|
30
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry;
|
|
31
|
+
return new KeyPair(privateKeyEntry.getCertificate().getPublicKey(), privateKeyEntry.getPrivateKey());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
|
|
36
|
+
KeyProperties.KEY_ALGORITHM_EC,
|
|
37
|
+
ANDROID_KEYSTORE
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
|
|
41
|
+
KEY_ALIAS,
|
|
42
|
+
KeyProperties.PURPOSE_AGREE_KEY
|
|
43
|
+
)
|
|
44
|
+
.setAlgorithmParameterSpec(new java.security.spec.ECGenParameterSpec("secp256r1"))
|
|
45
|
+
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
46
|
+
.build();
|
|
47
|
+
|
|
48
|
+
keyPairGenerator.initialize(spec);
|
|
49
|
+
return keyPairGenerator.generateKeyPair();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static String getPublicKeyBase64(Context context) throws Exception {
|
|
53
|
+
PublicKey publicKey = getOrCreateKeyPair(context).getPublic();
|
|
54
|
+
return Base64.encodeToString(publicKey.getEncoded(), Base64.NO_WRAP);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public static PrivateKey getPrivateKey(Context context) throws Exception {
|
|
58
|
+
return getOrCreateKeyPair(context).getPrivate();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.securitysuite;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
4
|
+
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
5
|
+
import com.facebook.react.bridge.ReadableType;
|
|
6
|
+
|
|
7
|
+
import java.util.HashMap;
|
|
8
|
+
import java.util.Locale;
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
import java.util.Set;
|
|
11
|
+
|
|
12
|
+
import okhttp3.Headers;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prevents sensitive credentials from appearing in debug network logs.
|
|
16
|
+
*/
|
|
17
|
+
public final class HeaderSanitizer {
|
|
18
|
+
public static final Set<String> SENSITIVE_HEADERS = Set.of(
|
|
19
|
+
"authorization",
|
|
20
|
+
"proxy-authorization",
|
|
21
|
+
"cookie",
|
|
22
|
+
"set-cookie",
|
|
23
|
+
"x-api-key",
|
|
24
|
+
"x-auth-token",
|
|
25
|
+
"x-access-token",
|
|
26
|
+
"x-jws-signature",
|
|
27
|
+
"x-request-signature",
|
|
28
|
+
"x-csrf-token"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
private HeaderSanitizer() {}
|
|
32
|
+
|
|
33
|
+
public static boolean isSensitiveHeader(String name) {
|
|
34
|
+
return name != null && SENSITIVE_HEADERS.contains(name.toLowerCase(Locale.US));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public static String maskValue(String value) {
|
|
38
|
+
if (value == null || value.isEmpty()) {
|
|
39
|
+
return "***";
|
|
40
|
+
}
|
|
41
|
+
if (value.length() <= 8) {
|
|
42
|
+
return "***";
|
|
43
|
+
}
|
|
44
|
+
return value.substring(0, 4) + "***" + value.substring(value.length() - 2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static Headers sanitizeHeaders(Headers headers) {
|
|
48
|
+
if (headers == null) {
|
|
49
|
+
return new Headers.Builder().build();
|
|
50
|
+
}
|
|
51
|
+
Headers.Builder builder = new Headers.Builder();
|
|
52
|
+
for (int i = 0; i < headers.size(); i++) {
|
|
53
|
+
String name = headers.name(i);
|
|
54
|
+
String value = headers.value(i);
|
|
55
|
+
builder.add(name, isSensitiveHeader(name) ? maskValue(value) : value);
|
|
56
|
+
}
|
|
57
|
+
return builder.build();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static Map<String, String> sanitizeReadableMap(ReadableMap readableMap) {
|
|
61
|
+
HashMap<String, String> map = new HashMap<>();
|
|
62
|
+
if (readableMap == null) {
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
|
|
66
|
+
while (iterator.hasNextKey()) {
|
|
67
|
+
String key = iterator.nextKey();
|
|
68
|
+
if (readableMap.getType(key) == ReadableType.String) {
|
|
69
|
+
String value = readableMap.getString(key);
|
|
70
|
+
map.put(key, isSensitiveHeader(key) ? maskValue(value) : value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,45 +1,250 @@
|
|
|
1
1
|
package com.securitysuite;
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
4
|
+
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
5
|
+
import com.facebook.react.bridge.ReadableType;
|
|
6
|
+
|
|
7
|
+
import org.json.JSONObject;
|
|
4
8
|
|
|
5
9
|
import javax.crypto.Mac;
|
|
6
|
-
import javax.crypto.
|
|
10
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
7
11
|
|
|
8
12
|
import java.nio.charset.StandardCharsets;
|
|
9
|
-
import java.
|
|
10
|
-
import java.security.NoSuchAlgorithmException;
|
|
13
|
+
import java.util.TreeMap;
|
|
11
14
|
|
|
15
|
+
/**
|
|
16
|
+
* RFC 7515 compact JWS serialization.
|
|
17
|
+
* Protected headers are built with JSONObject — never string concatenation.
|
|
18
|
+
*/
|
|
12
19
|
public class JWSGenerator {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
public String generate(
|
|
21
|
+
String payloadString,
|
|
22
|
+
String secret,
|
|
23
|
+
String algorithm,
|
|
24
|
+
ReadableMap headers,
|
|
25
|
+
boolean detached
|
|
26
|
+
) throws Exception {
|
|
27
|
+
if (secret == null || secret.trim().isEmpty()) {
|
|
28
|
+
throw new IllegalArgumentException("JWS secret is required and must be a non-empty string");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
String selectedAlgorithm = resolveAlgorithm(algorithm, headers);
|
|
32
|
+
JSONObject header = buildProtectedHeader(selectedAlgorithm, headers);
|
|
33
|
+
|
|
34
|
+
byte[] headerBytes = serializeHeader(header);
|
|
35
|
+
String encodedProtectedHeader = CryptoUtils.base64UrlEncode(headerBytes);
|
|
36
|
+
String encodedPayload = encodePayload(payloadString);
|
|
37
|
+
byte[] signingInput = buildSigningInputBytes(encodedProtectedHeader, encodedPayload);
|
|
38
|
+
|
|
39
|
+
Mac mac = Mac.getInstance(CryptoUtils.hmacAlgorithmForJws(selectedAlgorithm));
|
|
40
|
+
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), CryptoUtils.hmacAlgorithmForJws(selectedAlgorithm)));
|
|
41
|
+
String encodedSignature = CryptoUtils.base64UrlEncode(mac.doFinal(signingInput));
|
|
42
|
+
|
|
43
|
+
return formatCompactJws(encodedProtectedHeader, encodedPayload, encodedSignature, detached);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @deprecated Use {@link #generate} with explicit secret and headers. */
|
|
47
|
+
@Deprecated
|
|
48
|
+
public String jwsHeader(byte[] payload, String keyId, String requestId, String secret) {
|
|
49
|
+
try {
|
|
50
|
+
JSONObject legacyHeaders = new JSONObject();
|
|
51
|
+
legacyHeaders.put("kid", keyId);
|
|
52
|
+
legacyHeaders.put("request_id", requestId);
|
|
53
|
+
|
|
54
|
+
String payloadString = payload != null
|
|
55
|
+
? new String(payload, StandardCharsets.UTF_8)
|
|
56
|
+
: "";
|
|
57
|
+
|
|
58
|
+
return generate(payloadString, secret, "HS256", toReadableMap(legacyHeaders), true);
|
|
59
|
+
} catch (Exception e) {
|
|
60
|
+
throw new IllegalStateException("JWS generation failed: " + e.getMessage(), e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public boolean verify(
|
|
65
|
+
String compactJws,
|
|
66
|
+
String payloadString,
|
|
67
|
+
String secret,
|
|
68
|
+
String expectedAlgorithm,
|
|
69
|
+
boolean detached
|
|
70
|
+
) throws Exception {
|
|
71
|
+
if (compactJws == null || compactJws.isEmpty()) {
|
|
72
|
+
throw new IllegalArgumentException("Invalid compact JWS format");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
String encodedProtectedHeader;
|
|
76
|
+
String encodedSignature;
|
|
77
|
+
|
|
78
|
+
if (detached) {
|
|
79
|
+
String[] parts = compactJws.split("\\.\\.", 2);
|
|
80
|
+
if (parts.length != 2) {
|
|
81
|
+
throw new IllegalArgumentException("Invalid compact detached JWS format");
|
|
82
|
+
}
|
|
83
|
+
encodedProtectedHeader = parts[0];
|
|
84
|
+
encodedSignature = parts[1];
|
|
85
|
+
} else {
|
|
86
|
+
String[] parts = compactJws.split("\\.");
|
|
87
|
+
if (parts.length != 3) {
|
|
88
|
+
throw new IllegalArgumentException("Invalid compact JWS format");
|
|
89
|
+
}
|
|
90
|
+
encodedProtectedHeader = parts[0];
|
|
91
|
+
encodedSignature = parts[2];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
String encodedPayload = encodePayload(payloadString);
|
|
95
|
+
byte[] signingInput = buildSigningInputBytes(encodedProtectedHeader, encodedPayload);
|
|
96
|
+
|
|
97
|
+
String algorithm = expectedAlgorithm != null ? expectedAlgorithm : "HS256";
|
|
98
|
+
Mac mac = Mac.getInstance(CryptoUtils.hmacAlgorithmForJws(CryptoUtils.validateJwsAlgorithm(algorithm)));
|
|
99
|
+
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), CryptoUtils.hmacAlgorithmForJws(algorithm)));
|
|
100
|
+
byte[] expectedSignature = mac.doFinal(signingInput);
|
|
101
|
+
|
|
102
|
+
byte[] providedSignature = android.util.Base64.decode(
|
|
103
|
+
encodedSignature,
|
|
104
|
+
android.util.Base64.URL_SAFE | android.util.Base64.NO_PADDING | android.util.Base64.NO_WRAP
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return java.security.MessageDigest.isEqual(expectedSignature, providedSignature);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static String resolveAlgorithm(String algorithm, ReadableMap headers) throws Exception {
|
|
111
|
+
String headerAlg = null;
|
|
112
|
+
if (headers != null && headers.hasKey("alg") && !headers.isNull("alg")) {
|
|
113
|
+
headerAlg = readHeaderValueAsString(headers, "alg");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (algorithm != null && !algorithm.isEmpty() && headerAlg != null && !algorithm.equals(headerAlg)) {
|
|
117
|
+
throw new IllegalArgumentException(
|
|
118
|
+
"JWS algorithm mismatch: options.algorithm and headers.alg must match"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (algorithm != null && !algorithm.isEmpty()) {
|
|
123
|
+
return CryptoUtils.validateJwsAlgorithm(algorithm);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (headerAlg != null && !headerAlg.isEmpty()) {
|
|
127
|
+
return CryptoUtils.validateJwsAlgorithm(headerAlg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return "HS256";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private JSONObject buildProtectedHeader(String selectedAlgorithm, ReadableMap headers) throws Exception {
|
|
134
|
+
JSONObject header = new JSONObject();
|
|
135
|
+
header.put("alg", selectedAlgorithm);
|
|
136
|
+
|
|
137
|
+
if (headers != null) {
|
|
138
|
+
TreeMap<String, Object> sorted = new TreeMap<>();
|
|
139
|
+
ReadableMapKeySetIterator iterator = headers.keySetIterator();
|
|
140
|
+
while (iterator.hasNextKey()) {
|
|
141
|
+
String key = iterator.nextKey();
|
|
142
|
+
if ("alg".equals(key)) {
|
|
143
|
+
continue;
|
|
35
144
|
}
|
|
145
|
+
CryptoUtils.validateJwsHeaderKey(key);
|
|
146
|
+
sorted.put(key, readHeaderValue(headers, key));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (java.util.Map.Entry<String, Object> entry : sorted.entrySet()) {
|
|
150
|
+
header.put(entry.getKey(), entry.getValue());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return header;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private static Object readHeaderValue(ReadableMap headers, String key) {
|
|
158
|
+
ReadableType type = headers.getType(key);
|
|
159
|
+
switch (type) {
|
|
160
|
+
case Null:
|
|
161
|
+
return JSONObject.NULL;
|
|
162
|
+
case Boolean:
|
|
163
|
+
return headers.getBoolean(key);
|
|
164
|
+
case Number:
|
|
165
|
+
return headers.getDouble(key);
|
|
166
|
+
case String:
|
|
167
|
+
String value = headers.getString(key);
|
|
168
|
+
CryptoUtils.validateJwsHeaderValue(value);
|
|
169
|
+
return value;
|
|
170
|
+
default:
|
|
171
|
+
throw new IllegalArgumentException(
|
|
172
|
+
"JWS header values must be JSON-serializable primitives: " + key
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static String readHeaderValueAsString(ReadableMap headers, String key) {
|
|
178
|
+
ReadableType type = headers.getType(key);
|
|
179
|
+
if (type == ReadableType.String) {
|
|
180
|
+
return headers.getString(key);
|
|
181
|
+
}
|
|
182
|
+
if (type == ReadableType.Number) {
|
|
183
|
+
double number = headers.getDouble(key);
|
|
184
|
+
if (number == Math.rint(number)) {
|
|
185
|
+
return String.valueOf((long) number);
|
|
186
|
+
}
|
|
187
|
+
return String.valueOf(number);
|
|
188
|
+
}
|
|
189
|
+
if (type == ReadableType.Boolean) {
|
|
190
|
+
return String.valueOf(headers.getBoolean(key));
|
|
191
|
+
}
|
|
192
|
+
throw new IllegalArgumentException(
|
|
193
|
+
"JWS header values must be JSON-serializable primitives: " + key
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private static byte[] serializeHeader(JSONObject header) throws Exception {
|
|
198
|
+
TreeMap<String, Object> sorted = new TreeMap<>();
|
|
199
|
+
java.util.Iterator<String> keys = header.keys();
|
|
200
|
+
while (keys.hasNext()) {
|
|
201
|
+
String key = keys.next();
|
|
202
|
+
sorted.put(key, header.get(key));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
JSONObject sortedHeader = new JSONObject();
|
|
206
|
+
for (java.util.Map.Entry<String, Object> entry : sorted.entrySet()) {
|
|
207
|
+
sortedHeader.put(entry.getKey(), entry.getValue());
|
|
208
|
+
}
|
|
209
|
+
return sortedHeader.toString().getBytes(StandardCharsets.UTF_8);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static String encodePayload(String payloadString) {
|
|
213
|
+
if (payloadString == null || payloadString.isEmpty()) {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
return CryptoUtils.base64UrlEncode(payloadString.getBytes(StandardCharsets.UTF_8));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static byte[] buildSigningInputBytes(String encodedProtectedHeader, String encodedPayload) {
|
|
220
|
+
String signingInput = encodedProtectedHeader + "." + (encodedPayload != null ? encodedPayload : "");
|
|
221
|
+
return signingInput.getBytes(StandardCharsets.UTF_8);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static String formatCompactJws(
|
|
225
|
+
String encodedProtectedHeader,
|
|
226
|
+
String encodedPayload,
|
|
227
|
+
String encodedSignature,
|
|
228
|
+
boolean detached
|
|
229
|
+
) {
|
|
230
|
+
if (detached) {
|
|
231
|
+
return encodedProtectedHeader + ".." + encodedSignature;
|
|
36
232
|
}
|
|
233
|
+
String payloadSegment = encodedPayload != null ? encodedPayload : "";
|
|
234
|
+
return encodedProtectedHeader + "." + payloadSegment + "." + encodedSignature;
|
|
235
|
+
}
|
|
37
236
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
237
|
+
private static ReadableMap toReadableMap(JSONObject jsonObject) {
|
|
238
|
+
com.facebook.react.bridge.WritableMap map = com.facebook.react.bridge.Arguments.createMap();
|
|
239
|
+
java.util.Iterator<String> keys = jsonObject.keys();
|
|
240
|
+
while (keys.hasNext()) {
|
|
241
|
+
String key = keys.next();
|
|
242
|
+
try {
|
|
243
|
+
map.putString(key, jsonObject.getString(key));
|
|
244
|
+
} catch (Exception e) {
|
|
245
|
+
throw new IllegalStateException("Failed to convert legacy JWS headers", e);
|
|
246
|
+
}
|
|
44
247
|
}
|
|
248
|
+
return map;
|
|
249
|
+
}
|
|
45
250
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package com.securitysuite;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
4
|
+
import com.facebook.react.bridge.ReadableType;
|
|
5
|
+
|
|
6
|
+
import org.json.JSONObject;
|
|
7
|
+
|
|
8
|
+
import java.net.URI;
|
|
9
|
+
import java.nio.charset.StandardCharsets;
|
|
10
|
+
import java.util.TreeMap;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds the default fetch request-signing payload when jws.payload is omitted.
|
|
14
|
+
*/
|
|
15
|
+
public final class JwsFetchPayload {
|
|
16
|
+
private JwsFetchPayload() {}
|
|
17
|
+
|
|
18
|
+
public static String build(
|
|
19
|
+
String url,
|
|
20
|
+
String method,
|
|
21
|
+
byte[] requestBody,
|
|
22
|
+
ReadableMap jwsOptions
|
|
23
|
+
) throws Exception {
|
|
24
|
+
if (jwsOptions != null && jwsOptions.hasKey("payload")) {
|
|
25
|
+
ReadableType payloadType = jwsOptions.getType("payload");
|
|
26
|
+
if (payloadType == ReadableType.Null) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
if (payloadType == ReadableType.String) {
|
|
30
|
+
String payload = jwsOptions.getString("payload");
|
|
31
|
+
return payload != null ? payload : "";
|
|
32
|
+
}
|
|
33
|
+
throw new IllegalArgumentException("JWS fetch payload must be a string when provided");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
URI uri = new URI(url);
|
|
37
|
+
TreeMap<String, Object> fields = new TreeMap<>();
|
|
38
|
+
fields.put("method", method != null ? method.toUpperCase() : "GET");
|
|
39
|
+
fields.put("path", uri.getPath() != null && !uri.getPath().isEmpty() ? uri.getPath() : "/");
|
|
40
|
+
|
|
41
|
+
if (uri.getQuery() != null && !uri.getQuery().isEmpty()) {
|
|
42
|
+
fields.put("query", uri.getQuery());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (requestBody != null && requestBody.length > 0) {
|
|
46
|
+
fields.put("bodyHash", CryptoUtils.base64UrlEncode(CryptoUtils.sha256(requestBody)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (jwsOptions != null && jwsOptions.hasKey("headers") && jwsOptions.getMap("headers") != null) {
|
|
50
|
+
ReadableMap headers = jwsOptions.getMap("headers");
|
|
51
|
+
copyIfPresent(headers, fields, "timestamp");
|
|
52
|
+
copyIfPresent(headers, fields, "nonce");
|
|
53
|
+
copyIfPresent(headers, fields, "request_id");
|
|
54
|
+
copyIfPresent(headers, fields, "requestId");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
JSONObject json = new JSONObject();
|
|
58
|
+
for (java.util.Map.Entry<String, Object> entry : fields.entrySet()) {
|
|
59
|
+
json.put(entry.getKey(), entry.getValue());
|
|
60
|
+
}
|
|
61
|
+
return json.toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static void copyIfPresent(
|
|
65
|
+
ReadableMap headers,
|
|
66
|
+
TreeMap<String, Object> fields,
|
|
67
|
+
String key
|
|
68
|
+
) {
|
|
69
|
+
if (!headers.hasKey(key) || headers.isNull(key)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
ReadableType type = headers.getType(key);
|
|
73
|
+
if (type == ReadableType.String) {
|
|
74
|
+
fields.put(key, headers.getString(key));
|
|
75
|
+
} else if (type == ReadableType.Number) {
|
|
76
|
+
fields.put(key, headers.getDouble(key));
|
|
77
|
+
} else if (type == ReadableType.Boolean) {
|
|
78
|
+
fields.put(key, headers.getBoolean(key));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|