Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Support client certificates for SSL (two-way authentication) (Android) #2624

Merged
merged 18 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.network.CustomClientBuilder;
import okhttp3.OkHttpClient;
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
Expand All @@ -33,7 +34,7 @@ public static void initializeFlipper(Context context, ReactInstanceManager react
client.addPlugin(CrashReporterPlugin.getInstance());
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
new NetworkingModule.CustomClientBuilder() {
new CustomClientBuilder() {
@Override
public void apply(OkHttpClient.Builder builder) {
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
tools:replace="android:allowBackup"
>
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ protected List<ReactPackage> getPackages() {
packages.add(new KeyboardInputPackage(MainApplication.this));
packages.add(new WatermelonDBPackage());
packages.add(new RNCViewPagerPackage());
packages.add(new SSLPinningPackage());
List<ReactPackage> unimodules = Arrays.<ReactPackage>asList(
new ModuleRegistryAdapter(mModuleRegistryProvider)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package chat.rocket.reactnative;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.network.CustomClientBuilder;
import com.facebook.react.modules.network.ReactCookieJarContainer;
import com.facebook.react.modules.websocket.WebSocketModule;
import com.facebook.react.modules.fresco.ReactOkHttpNetworkFetcher;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;

import java.net.Socket;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509ExtendedKeyManager;
import java.security.PrivateKey;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import okhttp3.OkHttpClient;
import java.lang.InterruptedException;
import android.app.Activity;
import javax.net.ssl.KeyManager;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import java.util.concurrent.TimeUnit;

import com.RNFetchBlob.RNFetchBlob;

import com.reactnativecommunity.webview.RNCWebViewManager;

import com.dylanvann.fastimage.FastImageOkHttpUrlLoader;

import expo.modules.av.player.datasource.SharedCookiesDataSourceFactory;

public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyChainAliasCallback {

private Promise promise;
private static String alias;
private static ReactApplicationContext reactContext;

public SSLPinningModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

public class CustomClient implements CustomClientBuilder {
@Override
public void apply(OkHttpClient.Builder builder) {
if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory);
}
}
}
}

protected OkHttpClient getOkHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(new ReactCookieJarContainer());

if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory);
}
}

return builder.build();
}

@Override
public String getName() {
return "SSLPinning";
}

@Override
public void alias(String alias) {
this.alias = alias;

this.promise.resolve(alias);
}

@ReactMethod
public void setCertificate(String data, Promise promise) {
this.alias = data;

// HTTP Fetch react-native layer
NetworkingModule.setCustomClientBuilder(new CustomClient());
// Websocket react-native layer
WebSocketModule.setCustomClientBuilder(new CustomClient());
// Image networking react-native layer
ReactOkHttpNetworkFetcher.setOkHttpClient(getOkHttpClient());
// RNFetchBlob networking layer
RNFetchBlob.applyCustomOkHttpClient(getOkHttpClient());
// RNCWebView onReceivedClientCertRequest
RNCWebViewManager.setCertificateAlias(data);
// FastImage Glide network layer
FastImageOkHttpUrlLoader.setOkHttpClient(getOkHttpClient());
// Expo AV network layer
SharedCookiesDataSourceFactory.setOkHttpClient(getOkHttpClient());

promise.resolve(null);
}

@ReactMethod
public void pickCertificate(Promise promise) {
Activity activity = getCurrentActivity();

this.promise = promise;

KeyChain.choosePrivateKeyAlias(activity,
this, // Callback
null, // Any key types.
null, // Any issuers.
null, // Any host
-1, // Any port
"RocketChat");
}

public static SSLSocketFactory getSSLFactory(final String alias) {
try {
final PrivateKey privKey = KeyChain.getPrivateKey(reactContext, alias);
final X509Certificate[] certChain = KeyChain.getCertificateChain(reactContext, alias);

final X509ExtendedKeyManager keyManager = new X509ExtendedKeyManager() {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
return alias;
}

@Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return alias;
}

@Override
public X509Certificate[] getCertificateChain(String s) {
return certChain;
}

@Override
public String[] getClientAliases(String s, Principal[] principals) {
return new String[]{alias};
}

@Override
public String[] getServerAliases(String s, Principal[] principals) {
return new String[]{alias};
}

@Override
public PrivateKey getPrivateKey(String s) {
return privKey;
}
};

final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}

@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}

@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return certChain;
}
}
};

final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(new KeyManager[]{keyManager}, trustAllCerts, new java.security.SecureRandom());
SSLContext.setDefault(sslContext);

final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

return sslSocketFactory;
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package chat.rocket.reactnative;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;

public class SSLPinningPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new SSLPinningModule(reactContext));
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ buildscript {
minSdkVersion = 23
compileSdkVersion = 29
targetSdkVersion = 29
glideVersion = "4.9.0"
glideVersion = "4.11.0"
kotlin_version = "1.3.50"
supportLibVersion = "28.0.0"
libre_build = !(isPlay.toBoolean())
Expand Down
3 changes: 1 addition & 2 deletions app/actions/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ export function selectServerFailure() {
};
}

export function serverRequest(server, certificate = null, username = null, fromServerHistory = false) {
export function serverRequest(server, username = null, fromServerHistory = false) {
return {
type: SERVER.REQUEST,
server,
certificate,
username,
fromServerHistory
};
Expand Down
10 changes: 10 additions & 0 deletions app/lib/rocketchat.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import database from './database';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import fetch from '../utils/fetch';
import SSLPinning from '../utils/sslPinning';

import { encryptionInit } from '../actions/encryption';
import { setUser, setLoginServices, loginRequest } from '../actions/login';
Expand Down Expand Up @@ -63,6 +64,7 @@ import { sanitizeLikeString } from './database/utils';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const CURRENT_SERVER = 'currentServer';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY';
export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY';
Expand All @@ -74,6 +76,7 @@ const STATUSES = ['offline', 'online', 'away', 'busy'];
const RocketChat = {
TOKEN_KEY,
CURRENT_SERVER,
CERTIFICATE_KEY,
callJitsi,
async subscribeRooms() {
if (!this.roomsSub) {
Expand Down Expand Up @@ -312,6 +315,13 @@ const RocketChat = {
async shareExtensionInit(server) {
database.setShareDB(server);

try {
const certificate = await UserPreferences.getStringAsync(`${ RocketChat.CERTIFICATE_KEY }-${ server }`);
await SSLPinning.setCertificate(certificate, server);
} catch {
// Do nothing
}

if (this.shareSDK) {
this.shareSDK.disconnect();
this.shareSDK = null;
Expand Down
16 changes: 9 additions & 7 deletions app/sagas/selectServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
import log, { logServerVersion } from '../utils/log';
import { extractHostname } from '../utils/server';
import I18n from '../i18n';
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
import UserPreferences from '../lib/userPreferences';
import { encryptionStop } from '../actions/encryption';
import SSLPinning from '../utils/sslPinning';

import { inquiryReset } from '../ee/omnichannel/actions/inquiry';

Expand Down Expand Up @@ -68,6 +68,10 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {

const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try {
// SSL Pinning - Read certificate alias and set it to be used by network requests
const certificate = yield UserPreferences.getStringAsync(`${ RocketChat.CERTIFICATE_KEY }-${ server }`);
yield SSLPinning.setCertificate(certificate, server);

yield put(inquiryReset());
yield put(encryptionStop());
const serversDB = database.servers;
Expand Down Expand Up @@ -138,13 +142,11 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
}
};

const handleServerRequest = function* handleServerRequest({
server, certificate, username, fromServerHistory
}) {
const handleServerRequest = function* handleServerRequest({ server, username, fromServerHistory }) {
try {
if (certificate) {
yield UserPreferences.setMapAsync(extractHostname(server), certificate);
}
// SSL Pinning - Read certificate alias and set it to be used by network requests
const certificate = yield UserPreferences.getStringAsync(`${ RocketChat.CERTIFICATE_KEY }-${ server }`);
yield SSLPinning.setCertificate(certificate, server);

const serverInfo = yield getServerInfo({ server });
const serversDB = database.servers;
Expand Down
Loading