Skip to content

Commit 054c789

Browse files
authored
feat(Android): Add TLS key & cert for server (#192)
1 parent 1a11469 commit 054c789

File tree

7 files changed

+439
-26
lines changed

7 files changed

+439
-26
lines changed

android/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ android {
1616
dependencies {
1717
//noinspection GradleDynamicVersion
1818
implementation 'com.facebook.react:react-native:+' // From node_modules
19+
// Bouncy Castle dependencies
20+
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
21+
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
1922
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.asterinet.react.tcpsocket;
2+
3+
public class KeystoreInfo {
4+
private String keystoreName;
5+
private String caAlias;
6+
private String certAlias;
7+
private String keyAlias;
8+
9+
public KeystoreInfo(String keystoreName, String caAlias, String certAlias, String keyAlias) {
10+
this.keystoreName = keystoreName;
11+
this.caAlias = (caAlias == null || caAlias.isEmpty()) ? "ca" : caAlias;
12+
this.certAlias = (certAlias == null || certAlias.isEmpty()) ? "cert" : certAlias;
13+
this.keyAlias = (keyAlias == null || keyAlias.isEmpty()) ? "key" : keyAlias;
14+
}
15+
16+
public String getKeystoreName() {
17+
return this.keystoreName;
18+
}
19+
20+
public void setKeystoreName(String keystoreName) {
21+
this.keystoreName = keystoreName;
22+
}
23+
24+
public String getCaAlias() {
25+
return this.caAlias;
26+
}
27+
28+
public void setCaAlias(String caAlias) {
29+
this.caAlias = caAlias;
30+
}
31+
32+
public String getCertAlias() {
33+
return this.certAlias;
34+
}
35+
36+
public void setCertAlias(String certAlias) {
37+
this.certAlias = certAlias;
38+
}
39+
40+
public String getKeyAlias() {
41+
return this.keyAlias;
42+
}
43+
44+
public void setKeyAlias(String keyAlias) {
45+
this.keyAlias = keyAlias;
46+
}
47+
48+
@Override
49+
public String toString() {
50+
return "KeystoreInfo{" +
51+
"keystoreName='" + this.keystoreName + '\'' +
52+
", caAlias='" + this.caAlias + '\'' +
53+
", certAlias='" + this.certAlias + '\'' +
54+
", keyAlias='" + this.keyAlias + '\'' +
55+
'}';
56+
}
57+
}
58+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.asterinet.react.tcpsocket;
2+
3+
public class ResolvableOption {
4+
private final String value;
5+
private final boolean needsResolution;
6+
7+
public ResolvableOption(String value, boolean needsResolution) {
8+
this.value = value;
9+
this.needsResolution = needsResolution;
10+
}
11+
12+
public String getValue() {
13+
return value;
14+
}
15+
16+
public boolean needsResolution() {
17+
return needsResolution;
18+
}
19+
}
20+

android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java

Lines changed: 240 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,56 @@
22

33
import android.annotation.SuppressLint;
44
import android.content.Context;
5+
import android.os.Build;
56

67
import androidx.annotation.NonNull;
78
import androidx.annotation.RawRes;
89

910
import java.io.IOException;
1011
import java.io.InputStream;
12+
import java.io.InputStreamReader;
13+
import java.io.ByteArrayInputStream;
14+
import java.math.BigInteger;
15+
import java.net.Socket;
1116
import java.net.URI;
1217
import java.security.GeneralSecurityException;
18+
import java.security.KeyFactory;
1319
import java.security.KeyStore;
20+
import java.security.PrivateKey;
1421
import java.security.SecureRandom;
1522
import java.security.cert.Certificate;
1623
import java.security.cert.CertificateFactory;
24+
import java.security.spec.PKCS8EncodedKeySpec;
1725
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;
1831

1932
import javax.net.ssl.KeyManagerFactory;
2033
import javax.net.ssl.SSLContext;
34+
import javax.net.ssl.SSLPeerUnverifiedException;
2135
import javax.net.ssl.SSLServerSocketFactory;
36+
import javax.net.ssl.SSLSession;
2237
import javax.net.ssl.SSLSocket;
2338
import javax.net.ssl.SSLSocketFactory;
2439
import javax.net.ssl.TrustManager;
2540
import javax.net.ssl.TrustManagerFactory;
26-
import javax.net.ssl.X509ExtendedKeyManager;
2741
import javax.net.ssl.X509TrustManager;
2842

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+
2952

3053
final class SSLCertificateHelper {
54+
3155
/**
3256
* Creates an SSLSocketFactory instance for use with all CAs provided.
3357
*
@@ -56,31 +80,113 @@ static SSLServerSocketFactory createServerSocketFactory(Context context, @NonNul
5680
return sslContext.getServerSocketFactory();
5781
}
5882

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+
59112
/**
60113
* Creates an SSLSocketFactory instance for use with the CA provided in the resource file.
61114
*
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
64120
* @return An SSLSocketFactory which trusts the provided CA when provided to network clients
65121
*/
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+
}
84190
}
85191

86192
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) {
113219
public void checkServerTrusted(X509Certificate[] chain, String authType) {
114220
}
115221
}
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+
}
116335
}

0 commit comments

Comments
 (0)