/* MemorizingTrustManager - a TrustManager which asks the user about invalid
* certificates and memorizes their decision.
*
* Copyright (c) 2010 Georg Lukas <georg@op-co.de>
*
* MemorizingTrustManager.java contains the actual trust manager and interface
* code to create a MemorizingActivity and obtain the results.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.tapchatapp.android.network.ssl;
import android.app.Activity;
import android.app.Application;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class MemorizingTrustManager implements X509TrustManager {
public static final String DECISION_INTENT = "de.duenndns.ssl.DECISION";
public static final String DECISION_INTENT_APP = DECISION_INTENT + ".app";
public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
public static final String DECISION_INTENT_FINGERPRINT = DECISION_INTENT + ".fingerprint";
public static final String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
private static final String TAG = "MemorizingTrustManager";
private static final String KEYSTORE_DIR = "KeyStore";
private static final String KEYSTORE_FILE = "KeyStore.bks";
private static final Map<Integer, MTMDecision> sDecisions = Maps.newHashMap();
private static int sLastDecisionId = 0;
private Context mContext;
private Handler mHandler;
private X509TrustManager mDefaultTrustManager;
private X509TrustManager mAppTrustManager;
public static X509TrustManager[] getInstanceList(Context c) {
return new X509TrustManager[] { new MemorizingTrustManager(c) };
}
public MemorizingTrustManager(Context context) {
mContext = context;
mHandler = new Handler();
mDefaultTrustManager = getTrustManager(null);
mAppTrustManager = getTrustManager(loadAppKeyStore());
}
private X509TrustManager getTrustManager(KeyStore ks) {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(ks);
for (TrustManager t : tmf.getTrustManagers()) {
if (t instanceof X509TrustManager) {
return (X509TrustManager)t;
}
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private File getKeyStoreFile() {
Application app = getApplication(mContext);
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
return new File(dir, KEYSTORE_FILE);
}
private KeyStore loadAppKeyStore() {
File file = getKeyStoreFile();
try {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
if (file.exists()) {
ks.load(new java.io.FileInputStream(file), "MTM".toCharArray());
}
return ks;
} catch (Exception ex) {
if (file.exists()) {
file.delete();
}
throw new RuntimeException(ex);
}
}
private void storeCert(X509Certificate[] chain) {
try {
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
store.load(null, null);
// Add all certs from chain to key store
for (X509Certificate c : chain) {
store.setCertificateEntry(c.getSubjectDN().toString(), c);
}
// Overwrite existing keystore
FileOutputStream stream = null;
try {
stream = new java.io.FileOutputStream(getKeyStoreFile());
store.store(stream, "MTM".toCharArray());
} finally {
if (stream != null) {
stream.close();
}
}
// reload trust manager with new store
mAppTrustManager = getTrustManager(store);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
checkCertTrusted(chain, authType, false);
}
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
checkCertTrusted(chain, authType, true);
}
@Override public X509Certificate[] getAcceptedIssuers() {
return mDefaultTrustManager.getAcceptedIssuers();
}
private void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer) throws CertificateException {
if (checkCertificate(mDefaultTrustManager, chain, authType, isServer)) {
return;
}
if (checkCertificate(mAppTrustManager, chain, authType, isServer)) {
return;
}
interact(chain);
}
private boolean checkCertificate(X509TrustManager manager, X509Certificate[] chain, String authType, boolean isServer) {
try {
if (isServer) {
manager.checkServerTrusted(chain, authType);
} else {
manager.checkClientTrusted(chain, authType);
}
return true;
} catch (CertificateException ae) {
return false;
}
}
private void interact(final X509Certificate[] chain) throws CertificateException {
final MTMDecision decision = createDecision();
IntentFilter filter = new IntentFilter(DECISION_INTENT + "/" + mContext.getPackageName());
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override public void onReceive(Context ctx, Intent intent) {
int decisionId = intent.getIntExtra(DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
int choice = intent.getIntExtra(DECISION_INTENT_CHOICE, MTMDecision.DECISION_INVALID);
MTMDecision decision = getDecision(decisionId);
if (decision == null) {
Log.e(TAG, "interactResult: aborting due to stale decision reference!");
return;
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (decision) {
decision.state = choice;
decision.notify();
}
}
};
mContext.registerReceiver(receiver, filter);
mHandler.post(new Runnable() {
@Override public void run() {
Intent intent = new Intent(mContext, MemorizingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + decision.id));
intent.putExtra(DECISION_INTENT_APP, mContext.getPackageName());
intent.putExtra(DECISION_INTENT_ID, decision.id);
intent.putExtra(DECISION_INTENT_FINGERPRINT, CertUtil.certHash(chain[0], CertUtil.SHA1));
mContext.startActivity(intent);
}
});
//noinspection EmptyCatchBlock
try {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (decision) {
decision.wait();
}
} catch (InterruptedException e) {
}
mContext.unregisterReceiver(receiver);
switch (decision.state) {
case MTMDecision.DECISION_ALWAYS:
storeCert(chain);
case MTMDecision.DECISION_ONCE:
break;
default:
throw new CertificateException();
}
}
private static MTMDecision createDecision() {
synchronized (sDecisions) {
sLastDecisionId++;
MTMDecision decision = new MTMDecision(sLastDecisionId);
sDecisions.put(sLastDecisionId, decision);
return decision;
}
}
private static MTMDecision getDecision(int decisionId) {
synchronized (sDecisions) {
return sDecisions.remove(decisionId);
}
}
private static Application getApplication(Context context) {
if (context instanceof Application) {
return (Application)context;
} else if (context instanceof Service) {
return ((Service)context).getApplication();
} else if (context instanceof Activity) {
return ((Activity)context).getApplication();
} else {
throw new IllegalArgumentException("context must be either Activity or Service!");
}
}
}