Skip to content

Commit

Permalink
WIP: implementing X509 client cert authentication (android "buffer" m…
Browse files Browse the repository at this point in the history
…ode)
  • Loading branch information
Sefa Ilkimen committed Apr 15, 2019
1 parent 620ce3f commit 4f3ff90
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 178 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/**
test/e2e-app-template/www/certificates/*.cer
test/e2e-app-template/www/certificates/*.pkcs
tags
.zedstate
npm-debug.log
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "2.0.9",
"description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning",
"scripts": {
"updatecert": "node ./scripts/update-test-cert.js",
"updatecert": "node ./scripts/update-e2e-server-cert.js && node ./scripts/update-e2e-client-cert.js",
"buildbrowser": "./scripts/build-test-app.sh --browser",
"testandroid": "npm run updatecert && ./scripts/build-test-app.sh --android --emulator && ./scripts/test-app.sh --android --emulator",
"testios": "npm run updatecert && ./scripts/build-test-app.sh --ios --emulator && ./scripts/test-app.sh --ios --emulator",
Expand Down
29 changes: 29 additions & 0 deletions scripts/update-e2e-client-cert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const fs = require('fs');
const https = require('https');
const path = require('path');

const SOURCE_URL = 'https://badssl.com/certs/badssl.com-client.p12';
const TARGET_PATH = path.join(__dirname, '../test/e2e-app-template/www/certificates/badssl-client-cert.pkcs');

const downloadPkcsContainer = (source, target) => new Promise((resolve, reject) => {
const file = fs.createWriteStream(target);

const req = https.get(source, response => {
response.pipe(file)
resolve(target);
});

req.on('error', error => {
return reject(error)
});

req.end();
});

console.log(`Updating client certificate from ${SOURCE_URL}`);

downloadPkcsContainer(SOURCE_URL, TARGET_PATH)
.catch(error => {
console.error(`Updating client certificate failed: ${error}`);
process.exit(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@ const getCert = hostname => new Promise((resolve, reject) => {
req.end();
});

console.log(`Updating test certificate from ${SOURCE_HOST}`);
console.log(`Updating server certificate from ${SOURCE_HOST}`);

getCert(SOURCE_HOST)
.then(cert => {
fs.writeFileSync(TARGET_PATH, cert.raw);
})
.then(cert => fs.writeFileSync(TARGET_PATH, cert.raw))
.catch(error => {
console.error(`Updating test cert failed: ${error}`);
console.error(`Updating server certificate failed: ${error}`);
process.exit(1);
});
61 changes: 51 additions & 10 deletions src/android/com/silkimen/cordovahttp/CordovaClientAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import android.security.KeyChainAliasCallback;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;

import org.apache.cordova.CallbackContext;

Expand All @@ -21,17 +25,22 @@ class CordovaClientAuth implements Runnable, KeyChainAliasCallback {
private static final String TAG = "Cordova-Plugin-HTTP";

private String mode;
private String filePath;
private String aliasString;
private byte[] rawPkcs;
private String pkcsPassword;
private Activity activity;
private Context context;
private TLSConfiguration tlsConfiguration;
private CallbackContext callbackContext;

public CordovaClientAuth(final String mode, final String filePath, final Activity activity, final Context context,
final TLSConfiguration configContainer, final CallbackContext callbackContext) {
public CordovaClientAuth(final String mode, final String aliasString, final byte[] rawPkcs,
final String pkcsPassword, final Activity activity, final Context context, final TLSConfiguration configContainer,
final CallbackContext callbackContext) {

this.mode = mode;
this.filePath = filePath;
this.aliasString = aliasString;
this.rawPkcs = rawPkcs;
this.pkcsPassword = pkcsPassword;
this.activity = activity;
this.tlsConfiguration = configContainer;
this.context = context;
Expand All @@ -41,15 +50,45 @@ public CordovaClientAuth(final String mode, final String filePath, final Activit
@Override
public void run() {
if ("systemstore".equals(this.mode)) {
this.loadFromSystemStore();
} else if ("buffer".equals(this.mode)) {
this.loadFromBuffer();
} else {
this.disableClientAuth();
}
}

private void loadFromSystemStore() {
if (this.aliasString == null) {
KeyChain.choosePrivateKeyAlias(this.activity, this, null, null, null, -1, null);
} else if ("file".equals(this.mode)) {
this.callbackContext.error("Not implemented, yet");
} else {
this.tlsConfiguration.setKeyManagers(null);
this.alias(this.aliasString);
}
}

private void loadFromBuffer() {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm);
ByteArrayInputStream stream = new ByteArrayInputStream(this.rawPkcs);

keyStore.load(stream, this.pkcsPassword.toCharArray());
keyManagerFactory.init(keyStore, this.pkcsPassword.toCharArray());

this.tlsConfiguration.setKeyManagers(keyManagerFactory.getKeyManagers());
this.callbackContext.success();
} catch (Exception e) {
Log.e(TAG, "Couldn't load given PKCS12 container for authentication", e);
this.callbackContext.error("Couldn't load given PKCS12 container for authentication");
}
}

private void disableClientAuth() {
this.tlsConfiguration.setKeyManagers(null);
this.callbackContext.success();
}

@Override
public void alias(final String alias) {
try {
Expand All @@ -63,10 +102,12 @@ public void alias(final String alias) {

this.tlsConfiguration.setKeyManagers(new KeyManager[] { keyManager });

this.callbackContext.success();
this.callbackContext.success(alias);
} catch (Exception e) {
Log.e(TAG, "Couldn't load private key and certificate pair for authentication", e);
this.callbackContext.error("Couldn't load private key and certificate pair for authentication");
Log.e(TAG, "Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication",
e);
this.callbackContext.error(
"Couldn't load private key and certificate pair with given alias \"" + alias + "\" for authentication");
}
}
}
8 changes: 6 additions & 2 deletions src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.json.JSONObject;

import android.util.Log;
import android.util.Base64;

import javax.net.ssl.TrustManagerFactory;

Expand Down Expand Up @@ -149,8 +150,11 @@ private boolean setServerTrustMode(final JSONArray args, final CallbackContext c
}

private boolean setClientAuthMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), args.getString(1), this.cordova.getActivity(),
this.cordova.getActivity().getApplicationContext(), this.tlsConfiguration, callbackContext);
byte[] pkcs = args.isNull(2) ? null : Base64.decode(args.getString(2), Base64.DEFAULT);

CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), args.isNull(1) ? null : args.getString(1),
pkcs, args.getString(3), this.cordova.getActivity(), this.cordova.getActivity().getApplicationContext(),
this.tlsConfiguration, callbackContext);

cordova.getThreadPool().execute(runnable);

Expand Down
6 changes: 0 additions & 6 deletions src/android/com/silkimen/http/TLSConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import com.silkimen.http.TLSSocketFactory;

Expand Down
116 changes: 85 additions & 31 deletions test/e2e-app-template/www/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ const app = {

lastResult: null,

initialize: function() {
initialize: function () {
document.getElementById('nextBtn').addEventListener('click', app.onNextBtnClick);
},

printResult: function(prefix, content) {
printResult: function (prefix, content) {
const text = prefix + ': ' + JSON.stringify(content);

document.getElementById('resultTextarea').value += text;
},

reject: function(content) {
reject: function (content) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - rejected', content);

Expand All @@ -23,7 +23,7 @@ const app = {
};
},

resolve: function(content) {
resolve: function (content) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - resolved', content);

Expand All @@ -33,7 +33,7 @@ const app = {
};
},

throw: function(error) {
throw: function (error) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - throwed', error.message);

Expand All @@ -43,11 +43,11 @@ const app = {
};
},

getResult: function(cb) {
getResult: function (cb) {
cb(app.lastResult);
},

runTest: function(index) {
runTest: function (index) {
const testDefinition = tests[index];
const titleText = app.testIndex + ': ' + testDefinition.description;
const expectedText = 'expected - ' + testDefinition.expected;
Expand All @@ -57,32 +57,88 @@ const app = {
document.getElementById('resultTextarea').value = '';
document.getElementById('descriptionLbl').innerText = titleText;

try {
testDefinition.func(app.resolve, app.reject);
} catch (error) {
app.throw(error);
}
},
const onSuccessFactory = function (cbChain) {
return function () {
cbChain.shift()(cbChain);
}
};

onBeforeTest: function(testIndex, cb) {
app.lastResult = null;
const onFailFactory = function (prefix) {
return function (errorMessage) {
app.reject(prefix + ': ' + errorMessage);
}
};

const onThrowedHandler = function (prefix, error) {
app.throw(new Error(prefix + ': ' + error.message));
};

if (hooks && hooks.onBeforeEachTest) {
return hooks.onBeforeEachTest(function() {
const testDefinition = tests[testIndex];
const execBeforeEachTest = function (cbChain) {
const prefix = 'in before each hook';

if (testDefinition.before) {
testDefinition.before(cb);
} else {
cb();
try {
if (!hooks || !hooks.onBeforeEachTest) {
return onSuccessFactory(cbChain)();
}
});
} else {
cb();
}

hooks.onBeforeEachTest(
onSuccessFactory(cbChain),
onFailFactory(prefix)
);
} catch (error) {
onThrowedHandler(prefix, error);
}
};

const execBeforeTest = function (cbChain) {
const prefix = 'in before hook';

try {
if (!testDefinition.before) {
return onSuccessFactory(cbChain)();
}

testDefinition.before(
onSuccessFactory(cbChain),
onFailFactory(prefix)
);
} catch (error) {
onThrowedHandler(prefix, error);
}
};

const execTest = function () {
try {
testDefinition.func(app.resolve, app.reject);
} catch (error) {
app.throw(error);
}
};

onSuccessFactory([execBeforeEachTest, execBeforeTest, execTest])();
},

onBeforeTest: function (testIndex, resolve, reject) {
const runBeforeEachTest = function (resolve, reject) {
if (!hooks || !hooks.onBeforeEachTest) return resolve();

hooks.onBeforeEachTest(resolve, reject);
};

const runBeforeTest = function (testIndex, resolve, reject) {
if (!tests[testIndex].before) return resolve();

tests[testIndex].before(resolve, reject);
};

app.lastResult = null;

runBeforeEachTest(function () {
runBeforeTest(testIndex, resolve);
}, reject);
},

onFinishedAllTests: function() {
onFinishedAllTests: function () {
const titleText = 'No more tests';
const expectedText = 'You have run all available tests.';

Expand All @@ -91,13 +147,11 @@ const app = {
document.getElementById('descriptionLbl').innerText = titleText;
},

onNextBtnClick: function() {
onNextBtnClick: function () {
app.testIndex += 1;

if (app.testIndex < tests.length) {
app.onBeforeTest(app.testIndex, function() {
app.runTest(app.testIndex);
});
app.runTest(app.testIndex);
} else {
app.onFinishedAllTests();
}
Expand Down
Loading

0 comments on commit 4f3ff90

Please sign in to comment.