|
2 | 2 |
|
3 | 3 | import android.annotation.SuppressLint;
|
4 | 4 | import android.content.Context;
|
| 5 | +import android.os.Build; |
5 | 6 |
|
6 | 7 | import androidx.annotation.NonNull;
|
7 | 8 | import androidx.annotation.RawRes;
|
8 | 9 |
|
9 | 10 | import java.io.IOException;
|
10 | 11 | import java.io.InputStream;
|
| 12 | +import java.io.InputStreamReader; |
| 13 | +import java.io.ByteArrayInputStream; |
| 14 | +import java.math.BigInteger; |
| 15 | +import java.net.Socket; |
11 | 16 | import java.net.URI;
|
12 | 17 | import java.security.GeneralSecurityException;
|
| 18 | +import java.security.KeyFactory; |
13 | 19 | import java.security.KeyStore;
|
| 20 | +import java.security.PrivateKey; |
14 | 21 | import java.security.SecureRandom;
|
15 | 22 | import java.security.cert.Certificate;
|
16 | 23 | import java.security.cert.CertificateFactory;
|
| 24 | +import java.security.spec.PKCS8EncodedKeySpec; |
17 | 25 | import java.security.cert.X509Certificate;
|
| 26 | +import java.security.interfaces.RSAPublicKey; |
| 27 | +import java.text.SimpleDateFormat; |
| 28 | +import java.util.Base64; |
| 29 | +import java.util.Date; |
| 30 | +import java.util.Locale; |
18 | 31 |
|
19 | 32 | import javax.net.ssl.KeyManagerFactory;
|
20 | 33 | import javax.net.ssl.SSLContext;
|
| 34 | +import javax.net.ssl.SSLPeerUnverifiedException; |
21 | 35 | import javax.net.ssl.SSLServerSocketFactory;
|
| 36 | +import javax.net.ssl.SSLSession; |
22 | 37 | import javax.net.ssl.SSLSocket;
|
23 | 38 | import javax.net.ssl.SSLSocketFactory;
|
24 | 39 | import javax.net.ssl.TrustManager;
|
25 | 40 | import javax.net.ssl.TrustManagerFactory;
|
26 |
| -import javax.net.ssl.X509ExtendedKeyManager; |
27 | 41 | import javax.net.ssl.X509TrustManager;
|
28 | 42 |
|
| 43 | +import com.facebook.react.bridge.Arguments; |
| 44 | +import com.facebook.react.bridge.ReadableMap; |
| 45 | +import com.facebook.react.bridge.WritableMap; |
| 46 | + |
| 47 | + |
| 48 | +import org.bouncycastle.util.io.pem.PemObject; |
| 49 | +import org.bouncycastle.util.io.pem.PemReader; |
| 50 | +import org.json.JSONObject; |
| 51 | + |
29 | 52 |
|
30 | 53 | final class SSLCertificateHelper {
|
| 54 | + |
31 | 55 | /**
|
32 | 56 | * Creates an SSLSocketFactory instance for use with all CAs provided.
|
33 | 57 | *
|
@@ -56,31 +80,113 @@ static SSLServerSocketFactory createServerSocketFactory(Context context, @NonNul
|
56 | 80 | return sslContext.getServerSocketFactory();
|
57 | 81 | }
|
58 | 82 |
|
| 83 | + public static PrivateKey getPrivateKeyFromPEM(InputStream keyStream) { |
| 84 | + try (PemReader pemReader = new PemReader(new InputStreamReader(keyStream))) { |
| 85 | + PemObject pemObject = pemReader.readPemObject(); |
| 86 | + byte[] pemContent = pemObject.getContent(); |
| 87 | + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pemContent); |
| 88 | + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
| 89 | + return keyFactory.generatePrivate(keySpec); |
| 90 | + } catch (Exception e) { |
| 91 | + throw new RuntimeException("Failed to parse private key from PEM", e); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /** |
| 96 | + * Creates an InpuStream either from a getRawResourceStream or from raw string |
| 97 | + * |
| 98 | + * @param context Context used to retrieve resource |
| 99 | + * @param optionRes ResolvableOption |
| 100 | + * @return An InputStream |
| 101 | + */ |
| 102 | + public static InputStream getResolvableinputStream( |
| 103 | + @NonNull final Context context, |
| 104 | + ResolvableOption optionRes) throws IOException { |
| 105 | + if (optionRes.needsResolution()) { |
| 106 | + return getRawResourceStream(context, optionRes.getValue()); |
| 107 | + } else { |
| 108 | + return new ByteArrayInputStream(optionRes.getValue().getBytes()); |
| 109 | + } |
| 110 | + } |
| 111 | + |
59 | 112 | /**
|
60 | 113 | * Creates an SSLSocketFactory instance for use with the CA provided in the resource file.
|
61 | 114 | *
|
62 |
| - * @param context Context used to open up the CA file |
63 |
| - * @param rawResourceUri Raw resource file to the CA (in .crt or .cer format, for instance) |
| 115 | + * @param context Context used to open up the CA file |
| 116 | + * @param optionResCa Raw resource file or string to the CA (in .crt or .cer format, for instance) |
| 117 | + * @param optionResKey Optional raw resource file or string to the Key (in .crt or .cer format, for instance) |
| 118 | + * @param optionResCert Optional raw resource file or string to the Cert (in .crt or .cer format, for instance) |
| 119 | + * @param keystoreInfo Information about keystore name and key/cert alias |
64 | 120 | * @return An SSLSocketFactory which trusts the provided CA when provided to network clients
|
65 | 121 | */
|
66 |
| - static SSLSocketFactory createCustomTrustedSocketFactory(@NonNull final Context context, @NonNull final String rawResourceUri) throws IOException, GeneralSecurityException { |
67 |
| - InputStream caInput = getRawResourceStream(context, rawResourceUri); |
68 |
| - // Generate the CA Certificate from the raw resource file |
69 |
| - Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); |
70 |
| - caInput.close(); |
71 |
| - // Load the key store using the CA |
72 |
| - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
73 |
| - keyStore.load(null, null); |
74 |
| - keyStore.setCertificateEntry("ca", ca); |
75 |
| - |
76 |
| - // Initialize the TrustManager with this CA |
77 |
| - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
78 |
| - tmf.init(keyStore); |
79 |
| - |
80 |
| - // Create an SSL context that uses the created trust manager |
81 |
| - SSLContext sslContext = SSLContext.getInstance("TLS"); |
82 |
| - sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); |
83 |
| - return sslContext.getSocketFactory(); |
| 122 | + static SSLSocketFactory createCustomTrustedSocketFactory( |
| 123 | + @NonNull final Context context, |
| 124 | + final ResolvableOption optionResCa, |
| 125 | + final ResolvableOption optionResKey, |
| 126 | + final ResolvableOption optionResCert, |
| 127 | + final KeystoreInfo keystoreInfo) throws IOException, GeneralSecurityException { |
| 128 | + |
| 129 | + SSLSocketFactory ssf = null; |
| 130 | + if (optionResCert != null && optionResKey != null) { |
| 131 | + final String keyStoreName = keystoreInfo.getKeystoreName().isEmpty() ? |
| 132 | + KeyStore.getDefaultType() : |
| 133 | + keystoreInfo.getKeystoreName(); |
| 134 | + KeyStore keyStore = KeyStore.getInstance(keyStoreName); |
| 135 | + keyStore.load(null, null); |
| 136 | + |
| 137 | + // Check if cert and key if already registered inside our keystore |
| 138 | + // If one is missing we insert again |
| 139 | + boolean hasCertInStore = keyStore.isCertificateEntry(keystoreInfo.getCertAlias()); |
| 140 | + boolean hasKeyInStore = keyStore.isKeyEntry(keystoreInfo.getKeyAlias()); |
| 141 | + if (!hasCertInStore || !hasKeyInStore) { |
| 142 | + InputStream certInput = getResolvableinputStream(context, optionResCert); |
| 143 | + Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(certInput); |
| 144 | + keyStore.setCertificateEntry(keystoreInfo.getCertAlias(), cert); |
| 145 | + |
| 146 | + InputStream keyInput = getResolvableinputStream(context, optionResKey); |
| 147 | + PrivateKey privateKey = getPrivateKeyFromPEM(keyInput); |
| 148 | + keyStore.setKeyEntry(keystoreInfo.getKeyAlias(), privateKey, null, new Certificate[]{cert}); |
| 149 | + } |
| 150 | + |
| 151 | + boolean hasCaInStore = keyStore.isCertificateEntry(keystoreInfo.getCaAlias()); |
| 152 | + if (optionResCa != null && !hasCaInStore) { |
| 153 | + InputStream caInput = getResolvableinputStream(context, optionResCa); |
| 154 | + // Generate the CA Certificate from the raw resource file |
| 155 | + Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); |
| 156 | + caInput.close(); |
| 157 | + // Load the key store using the CA |
| 158 | + keyStore.setCertificateEntry(keystoreInfo.getCaAlias(), ca); |
| 159 | + } |
| 160 | + |
| 161 | + // Initialize the KeyManagerFactory with this cert |
| 162 | + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
| 163 | + keyManagerFactory.init(keyStore, new char[0]); |
| 164 | + |
| 165 | + // Create an SSL context that uses the created trust manager |
| 166 | + SSLContext sslContext = SSLContext.getInstance("TLS"); |
| 167 | + sslContext.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{new BlindTrustManager()}, null); |
| 168 | + return sslContext.getSocketFactory(); |
| 169 | + |
| 170 | + } else { |
| 171 | + // Keep old behavior |
| 172 | + InputStream caInput = getResolvableinputStream(context, optionResCa); |
| 173 | + // Generate the CA Certificate from the raw resource file |
| 174 | + Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); |
| 175 | + caInput.close(); |
| 176 | + // Load the key store using the CA |
| 177 | + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| 178 | + keyStore.load(null, null); |
| 179 | + keyStore.setCertificateEntry("ca", ca); |
| 180 | + |
| 181 | + // Initialize the TrustManager with this CA |
| 182 | + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
| 183 | + tmf.init(keyStore); |
| 184 | + |
| 185 | + // Create an SSL context that uses the created trust manager |
| 186 | + SSLContext sslContext = SSLContext.getInstance("TLS"); |
| 187 | + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); |
| 188 | + return sslContext.getSocketFactory(); |
| 189 | + } |
84 | 190 | }
|
85 | 191 |
|
86 | 192 | private static InputStream getRawResourceStream(@NonNull final Context context, @NonNull final String resourceUri) throws IOException {
|
@@ -113,4 +219,117 @@ public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
113 | 219 | public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
114 | 220 | }
|
115 | 221 | }
|
| 222 | + |
| 223 | + public static ReadableMap getCertificateInfo(Socket socket, boolean wantPeerCert) { |
| 224 | + WritableMap certInfo = Arguments.createMap(); |
| 225 | + |
| 226 | + if (socket instanceof SSLSocket) { |
| 227 | + SSLSocket sslSocket = (SSLSocket) socket; |
| 228 | + try { |
| 229 | + SSLSession sslSession = sslSocket.getSession(); |
| 230 | + Certificate[] certificates = wantPeerCert ? sslSession.getPeerCertificates() : sslSession.getLocalCertificates(); |
| 231 | + if (certificates != null && certificates.length > 0 && certificates[0] instanceof X509Certificate) { |
| 232 | + X509Certificate cert = (X509Certificate) certificates[0]; |
| 233 | + WritableMap certDetails = Arguments.createMap(); |
| 234 | + certDetails.putMap("subject", parseDN(cert.getSubjectDN().getName())); |
| 235 | + certDetails.putMap("issuer", parseDN(cert.getIssuerDN().getName())); |
| 236 | + certDetails.putBoolean("ca", cert.getBasicConstraints() != -1); |
| 237 | + certDetails.putString("modulus", getModulus(cert)); |
| 238 | + certDetails.putInt("bits", getModulusBitLength(cert)); |
| 239 | + certDetails.putString("exponent", "0x" + getExponent(cert)); |
| 240 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| 241 | + certDetails.putString("pubkey", Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded())); |
| 242 | + } |
| 243 | + certDetails.putString("valid_from", formatDate(cert.getNotBefore())); |
| 244 | + certDetails.putString("valid_to", formatDate(cert.getNotAfter())); |
| 245 | + certDetails.putString("fingerprint", getFingerprint(cert, "SHA-1")); |
| 246 | + certDetails.putString("fingerprint256", getFingerprint(cert, "SHA-256")); |
| 247 | + certDetails.putString("fingerprint512", getFingerprint(cert, "SHA-512")); |
| 248 | + certDetails.putString("serialNumber", getSerialNumber(cert)); |
| 249 | + |
| 250 | + certInfo = certDetails; |
| 251 | + } |
| 252 | + } catch (SSLPeerUnverifiedException e) { |
| 253 | + throw new RuntimeException(e); |
| 254 | + } catch (Exception e) { |
| 255 | + throw new RuntimeException("Error processing certificate", e); |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + return certInfo; |
| 260 | + } |
| 261 | + |
| 262 | + // LdapName don't seem to be available on android .... |
| 263 | + // So very very dummy implementation |
| 264 | + // I can see inside android/platform/libcore an implementation but don't even know if we |
| 265 | + // can import it... |
| 266 | + //https://android.googlesource.com/platform/libcore/+/0ebbfbdbca73d6261a77183f68e1f3e56c339f9f/ojluni/src/main/java/javax/naming/ |
| 267 | + |
| 268 | + private static WritableMap parseDN(String dn) { |
| 269 | + WritableMap details = Arguments.createMap(); |
| 270 | + String[] components = dn.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); // Split by comma, but not inside quotes |
| 271 | + for (String component : components) { |
| 272 | + String[] keyValue = component.split("=", 2); |
| 273 | + if (keyValue.length == 2) { |
| 274 | + String key = keyValue[0].trim(); |
| 275 | + String value = keyValue[1].trim(); |
| 276 | + if ("2.5.4.46".equals(key)) { // OID for dnQualifier |
| 277 | + if (value.startsWith("#")) { |
| 278 | + String dnQualifier = decodeHexString(value.substring(1)); |
| 279 | + details.putString("dnQualifier", dnQualifier); |
| 280 | + } else { |
| 281 | + details.putString("dnQualifier", value); |
| 282 | + } |
| 283 | + } else if ("CN".equals(key)) { |
| 284 | + details.putString("CN", value); |
| 285 | + } |
| 286 | + } |
| 287 | + } |
| 288 | + return details; |
| 289 | + } |
| 290 | + |
| 291 | + private static String decodeHexString(String hex) { |
| 292 | + StringBuilder output = new StringBuilder(); |
| 293 | + for (int i = 0; i < hex.length(); i += 2) { |
| 294 | + String str = hex.substring(i, i + 2); |
| 295 | + output.append((char) Integer.parseInt(str, 16)); |
| 296 | + } |
| 297 | + // Remove leading control characters if they exist |
| 298 | + return output.toString().replaceAll("^\\p{Cntrl}", "").trim(); |
| 299 | + } |
| 300 | + |
| 301 | + private static String getSerialNumber(X509Certificate cert) { |
| 302 | + BigInteger serialNumber = cert.getSerialNumber(); |
| 303 | + return serialNumber.toString(16).toUpperCase(); // Convert to hex string and uppercase |
| 304 | + } |
| 305 | + private static String getModulus(X509Certificate cert) throws Exception { |
| 306 | + RSAPublicKey rsaPubKey = (RSAPublicKey) cert.getPublicKey(); |
| 307 | + return rsaPubKey.getModulus().toString(16).toUpperCase(); |
| 308 | + } |
| 309 | + |
| 310 | + private static int getModulusBitLength(X509Certificate cert) throws Exception { |
| 311 | + RSAPublicKey rsaPubKey = (RSAPublicKey) cert.getPublicKey(); |
| 312 | + return rsaPubKey.getModulus().bitLength(); |
| 313 | + } |
| 314 | + private static String getExponent(X509Certificate cert) throws Exception { |
| 315 | + RSAPublicKey rsaPubKey = (RSAPublicKey) cert.getPublicKey(); |
| 316 | + return rsaPubKey.getPublicExponent().toString(16).toUpperCase(); |
| 317 | + } |
| 318 | + |
| 319 | + private static String getFingerprint(X509Certificate cert, String algorithm) throws Exception { |
| 320 | + byte[] encoded = cert.getEncoded(); |
| 321 | + java.security.MessageDigest md = java.security.MessageDigest.getInstance(algorithm); |
| 322 | + byte[] digest = md.digest(encoded); |
| 323 | + StringBuilder sb = new StringBuilder(); |
| 324 | + for (byte b : digest) { |
| 325 | + sb.append(String.format("%02X:", b)); |
| 326 | + } |
| 327 | + return sb.substring(0, sb.length() - 1); // Remove the trailing colon |
| 328 | + } |
| 329 | + |
| 330 | + private static String formatDate(Date date) { |
| 331 | + SimpleDateFormat sdf = new SimpleDateFormat("MMM dd HH:mm:ss yyyy 'GMT'", Locale.US); |
| 332 | + sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT")); |
| 333 | + return sdf.format(date); |
| 334 | + } |
116 | 335 | }
|
0 commit comments