//на основе проекта MemorizingTrustManager - https://github.com/ge0rg/MemorizingTrustManager
/* 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 nya.miku.wishmaster.http.client;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import cz.msebera.android.httpclient.conn.ssl.X509HostnameVerifier;
import nya.miku.wishmaster.R;
import nya.miku.wishmaster.common.Logger;
public class ExtendedTrustManager implements X509TrustManager {
private static final String TAG = "ExtendedTrustManager";
public final static int DECISION_INVALID = 0;
public final static int DECISION_ABORT = 1;
public final static int DECISION_ONCE = 2;
public final static int DECISION_ALWAYS = 3;
private static final String KEYSTORE_DIR = "trusted_keystore";
private static final String KEYSTORE_FILE = "trusted_keystore.bks";
private static final String KEYSTORE_PASSWORD = "password";
private static Context staticContext;
private static Activity foregroundAct;
private Context context;
private X509TrustManager defaultTrustManager;
private File appKeyStoreFile;
private KeyStore appKeyStore;
private X509TrustManager appTrustManager;
/*package*/ ExtendedTrustManager() {
if (staticContext == null)
throw new IllegalStateException("set app context (call ExtendedTrustManager.setAppContext() in onCreate() of the application)");
context = staticContext;
appKeyStoreFile = new File(context.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE), KEYSTORE_FILE);
appKeyStore = loadAppKeyStore(appKeyStoreFile);
appTrustManager = getTrustManager(appKeyStore);
defaultTrustManager = getTrustManager(null);
}
public static void bindActivity(Activity activity) {
foregroundAct = activity;
}
public static void unbindActivity() {
foregroundAct = null;
}
public static void setAppContext(Context context) {
staticContext = context;
}
private static 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;
}
}
} catch (Exception e) {
// Here, we are covering up errors. It might be more useful
// however to throw them out of the constructor so the
// embedding app knows something went wrong.
Logger.e(TAG, "getTrustManager(" + ks + ")", e);
}
return null;
}
private static KeyStore loadAppKeyStore(File keyStoreFile) {
KeyStore ks;
try {
ks = KeyStore.getInstance(KeyStore.getDefaultType());
} catch (KeyStoreException e) {
Logger.e(TAG, "getAppKeyStore()", e);
return null;
}
try {
ks.load(null, null);
} catch (NoSuchAlgorithmException | CertificateException | IOException e) {
Logger.e(TAG, "getAppKeyStore(" + keyStoreFile + ")", e);
}
InputStream is = null;
try {
is = new java.io.FileInputStream(keyStoreFile);
ks.load(is, KEYSTORE_PASSWORD.toCharArray());
} catch (FileNotFoundException e) {
} catch (NoSuchAlgorithmException | CertificateException | IOException e) {
Logger.e(TAG, "getAppKeyStore(" + keyStoreFile + ") - exception loading file key store", e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
Logger.e(TAG, "getAppKeyStore(" + keyStoreFile + ") - exception closing file key store input stream", e);
}
}
}
return ks;
}
public Enumeration<String> getCertificates() {
try {
return appKeyStore.aliases();
} catch (KeyStoreException e) {
// this should never happen, however...
throw new RuntimeException(e);
}
}
public Certificate getCertificate(String alias) {
try {
return appKeyStore.getCertificate(alias);
} catch (KeyStoreException e) {
// this should never happen, however...
throw new RuntimeException(e);
}
}
public void deleteCertificate(String alias) throws KeyStoreException {
appKeyStore.deleteEntry(alias);
keyStoreUpdated();
}
private void storeCert(String alias, Certificate cert) {
try {
appKeyStore.setCertificateEntry(alias, cert);
} catch (KeyStoreException e) {
Logger.e(TAG, "storeCert(" + cert + ")", e);
return;
}
keyStoreUpdated();
}
private void storeCert(X509Certificate cert) {
storeCert(cert.getSubjectDN().toString(), cert);
}
private void keyStoreUpdated() {
// reload appTrustManager
appTrustManager = getTrustManager(appKeyStore);
// store KeyStore to file
java.io.FileOutputStream fos = null;
try {
fos = new java.io.FileOutputStream(appKeyStoreFile);
appKeyStore.store(fos, KEYSTORE_PASSWORD.toCharArray());
} catch (Exception e) {
Logger.e(TAG, "storeCert(" + appKeyStoreFile + ")", e);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
Logger.e(TAG, "storeCert(" + appKeyStoreFile + ")", 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 defaultTrustManager.getAcceptedIssuers();
}
/*package*/ X509HostnameVerifier wrapHostnameVerifier(final X509HostnameVerifier defaultVerifier) {
if (defaultVerifier == null) throw new IllegalArgumentException("The default verifier may not be null");
return new X509HostnameVerifier() {
private boolean verifyCert(String hostname, X509Certificate cert) {
try {
if (cert.equals(appKeyStore.getCertificate(hostname.toLowerCase(Locale.US)))) {
return true;
} else {
return interactHostname(cert, hostname);
}
} catch (Exception e) {
Logger.e(TAG, e);
return false;
}
}
@Override
public boolean verify(String hostname, SSLSession session) {
if (defaultVerifier.verify(hostname, session)) {
Logger.d(TAG, "default verifier accepted " + hostname);
return true;
}
try {
X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0];
return verifyCert(hostname, cert);
} catch (Exception e) {
Logger.e(TAG, e);
return false;
}
}
@Override
public void verify(String host, SSLSocket ssl) throws IOException {
try {
defaultVerifier.verify(host, ssl);
} catch (Exception e) {
X509Certificate cert = (X509Certificate) ssl.getSession().getPeerCertificates()[0];
if (verifyCert(host, cert)) return;
throw e;
}
}
@Override
public void verify(String host, X509Certificate cert) throws SSLException {
try {
defaultVerifier.verify(host, cert);
} catch (Exception e) {
if (verifyCert(host, cert)) return;
throw e;
}
}
@Override
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
defaultVerifier.verify(host, cns, subjectAlts);
}
};
}
private void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer) throws CertificateException {
try {
Logger.d(TAG, "checkCertTrusted: trying appTrustManager");
if (isServer) appTrustManager.checkServerTrusted(chain, authType);
else appTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) {
Logger.d(TAG, "checkCertTrusted: appTrustManager did not verify certificate. " +
"Will fall back to secondary verification mechanisms (if any). " + e);
// if the cert is stored in our appTrustManager, we ignore expiredness
if (isExpiredException(e)) {
Logger.d(TAG, "checkCertTrusted: accepting expired certificate from keystore");
return;
}
if (isCertKnown(chain[0])) {
Logger.d(TAG, "checkCertTrusted: accepting cert already stored in keystore");
return;
}
try {
if (defaultTrustManager == null) throw e;
Logger.d(TAG, "checkCertTrusted: trying defaultTrustManager");
if (isServer) defaultTrustManager.checkServerTrusted(chain, authType);
else defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e1) {
Logger.e(TAG, "checkCertTrusted: defaultTrustManager failed", e1);
interactCert(chain, authType, e1);
}
}
}
// if the certificate is stored in the app key store, it is considered
// "known"
private boolean isCertKnown(X509Certificate cert) {
try {
return appKeyStore.getCertificateAlias(cert) != null;
} catch (KeyStoreException e) {
return false;
}
}
private static boolean isExpiredException(Throwable e) {
while (e != null) {
if (e instanceof CertificateExpiredException) return true;
e = e.getCause();
}
return false;
}
private void interactCert(final X509Certificate[] chain, String authType, CertificateException cause) throws CertificateException {
switch (interact(certChainMessage(chain, cause), R.string.ssl_accept_cert)) {
case DECISION_ALWAYS:
storeCert(chain[0]); // only store the server cert, not the whole chain
case DECISION_ONCE:
break;
default:
throw cause;
}
}
private boolean interactHostname(X509Certificate cert, String hostname) {
switch (interact(hostNameMessage(cert, hostname), R.string.ssl_accept_servername)) {
case DECISION_ALWAYS:
storeCert(hostname, cert);
case DECISION_ONCE:
return true;
default:
return false;
}
}
private int interact(final String message, final int titleId) {
final Activity activity = foregroundAct;
if (activity == null) return DECISION_ABORT;
class Decision { int state = DECISION_INVALID; }
final Decision decision = new Decision();
class DlgRunnable implements Runnable, DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
@Override
public void run() {
AlertDialog dialog = new AlertDialog.Builder(activity).setTitle(titleId)
.setMessage(message)
.setPositiveButton(R.string.ssl_decision_always, this)
.setNeutralButton(R.string.ssl_decision_once, this)
.setNegativeButton(R.string.ssl_decision_abort, this)
.setOnCancelListener(this)
.create();
dialog.show();
}
@Override
public void onCancel(DialogInterface dialog) {
sendDecision(DECISION_ABORT);
}
@Override
public void onClick(DialogInterface dialog, int which) {
int decision;
dialog.dismiss();
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
decision = DECISION_ALWAYS;
break;
case DialogInterface.BUTTON_NEUTRAL:
decision = DECISION_ONCE;
break;
default:
decision = DECISION_ABORT;
}
sendDecision(decision);
}
void sendDecision(int d) {
Logger.d(TAG, "notify dicision " + d + "on " + decision);
synchronized (decision) {
decision.state = d;
decision.notify();
}
}
}
activity.runOnUiThread(new DlgRunnable());
Logger.d(TAG, "waiting on decision " + decision);
try {
synchronized (decision) {
decision.wait();
}
} catch (InterruptedException e) {
Logger.e(TAG, "InterruptedException", e);
}
Logger.d(TAG, "finished wait on " + decision);
return decision.state;
}
private String certChainMessage(final X509Certificate[] chain, CertificateException e) {
Throwable cause = e;
while (cause.getCause() != null) cause = cause.getCause();
StringBuilder si = new StringBuilder();
if (cause instanceof CertPathValidatorException) si.append(context.getString(R.string.ssl_trust_anchor));
else if (cause instanceof CertificateExpiredException) si.append(context.getString(R.string.ssl_cert_expired));
else si.append(cause.getLocalizedMessage() != null ? cause.getLocalizedMessage() : cause.getClass().getSimpleName());
si.append("\n\n");
si.append(context.getString(R.string.ssl_connect_anyway));
si.append("\n\n");
si.append(context.getString(R.string.ssl_cert_details));
si.append("\n");
certDetails(si, chain[0]);
return si.toString();
}
private String hostNameMessage(X509Certificate cert, String hostname) {
StringBuilder si = new StringBuilder();
si.append(context.getString(R.string.ssl_hostname_mismatch, hostname));
si.append("\n\n");
try {
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
if (sans == null) {
si.append(cert.getSubjectDN());
si.append("\n");
} else
for (List<?> altName : sans) {
Object name = altName.get(1);
if (name instanceof String) {
si.append("[");
si.append(altName.get(0));
si.append("] ");
si.append(name);
si.append("\n");
}
}
} catch (CertificateParsingException e) {
Logger.e(TAG, e);
si.append("<Parsing error: ");
si.append(e.getLocalizedMessage());
si.append(">\n");
}
si.append("\n");
si.append(context.getString(R.string.ssl_connect_anyway));
si.append("\n\n");
si.append(context.getString(R.string.ssl_cert_details));
si.append("\n");
certDetails(si, cert);
return si.toString();
}
public static void certDetails(StringBuilder si, X509Certificate c) {
SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
si.append(c.getSubjectDN().toString());
si.append("\n");
si.append(validityDateFormater.format(c.getNotBefore()));
si.append(" - ");
si.append(validityDateFormater.format(c.getNotAfter()));
si.append("\nSHA-256: ");
si.append(certHash(c, "SHA-256"));
si.append("\nSHA-1: ");
si.append(certHash(c, "SHA-1"));
si.append("\nSigned by: ");
si.append(c.getIssuerDN().toString());
}
private static String certHash(final X509Certificate cert, String digest) {
try {
MessageDigest md = MessageDigest.getInstance(digest);
md.update(cert.getEncoded());
return hexString(md.digest());
} catch (java.security.cert.CertificateEncodingException e) {
return e.getMessage();
} catch (java.security.NoSuchAlgorithmException e) {
return e.getMessage();
}
}
private static String hexString(byte[] data) {
StringBuilder si = new StringBuilder();
for (int i = 0; i < data.length; i++) {
si.append(String.format("%02X", data[i]));
if (i < data.length - 1) si.append(":");
}
return si.toString();
}
}