/*
* Copyright (C) 2014 Eric Butler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tapchatapp.android.network.ssl;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Base64;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
public class MemorizingHostnameVerifier implements HostnameVerifier {
public static final String EXTRA_DECISION_ID = "com.tapchatapp.android.EXTRA_DECISION_ID";
public static final String EXTRA_HOSTNAME = "com.tapchatapp.android.EXTRA_HOSTNAME";
public static final String EXTRA_FINGERPRINT = "com.tapchatapp.android.EXTRA_FINGERPRINT";
private static final String PREFS_FILENAME = "known_hosts";
private static int sLastDecisionId = 0;
private static final Map<Integer, Decision> sDecisions = new HashMap<>();
private final Context mContext;
private final SharedPreferences mPreferences;
private final Handler mHandler = new Handler();
private final Gson mGson = new Gson();
public MemorizingHostnameVerifier(Context context, Bus bus) {
mContext = context;
mPreferences = mContext.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE);
bus.register(this);
}
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
@Override public boolean verify(final String hostname, final SSLSession session) {
if (OkHostnameVerifier.INSTANCE.verify(hostname, session)) {
return true;
}
final byte[] encodedCertificate = CertUtil.getEncodedCertificate(session);
final String fingerprint = CertUtil.certHash(encodedCertificate, CertUtil.SHA1);
final String base64Certificate = Base64.encodeToString(encodedCertificate, Base64.DEFAULT);
if (getKnownCertificates(hostname).contains(base64Certificate)) {
return true;
}
final Decision decision = createDecision();
mHandler.post(new Runnable() {
@Override public void run() {
Intent intent = new Intent(mContext, VerifyHostnameActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(EXTRA_DECISION_ID, decision.id);
intent.putExtra(EXTRA_HOSTNAME, hostname);
intent.putExtra(EXTRA_FINGERPRINT, fingerprint);
mContext.startActivity(intent);
}
});
try {
synchronized (decision) {
decision.wait();
}
} catch (InterruptedException ignored) { }
if (decision.allow) {
addKnownCertificate(hostname, base64Certificate);
}
return decision.allow;
}
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
@Subscribe public void onHostnameVerifyDecisionEvent(HostnameVerifyDecisionEvent event) {
final Decision decision = getDecision(event.getDecisionId());
if (decision == null) {
return;
}
synchronized (decision) {
decision.allow = event.isDecisionAllow();
decision.notify();
}
}
private Set<String> getKnownCertificates(String hostname) {
if (!mPreferences.contains(hostname)) {
return new HashSet<>();
}
Type type = new TypeToken<Set<String>>() { }.getType();
return mGson.fromJson(mPreferences.getString(hostname, null), type);
}
private void addKnownCertificate(String hostname, String base64Certificate) {
Set<String> knownCertificates = new HashSet<>(getKnownCertificates(hostname));
knownCertificates.add(base64Certificate);
SharedPreferences.Editor editor = mPreferences.edit();
editor.putString(hostname, mGson.toJson(knownCertificates));
editor.apply();
}
private static Decision createDecision() {
synchronized (sDecisions) {
Decision decision = new Decision(sLastDecisionId);
sDecisions.put(sLastDecisionId, decision);
sLastDecisionId++;
return decision;
}
}
private static Decision getDecision(int decisionId) {
synchronized (sDecisions) {
return sDecisions.remove(decisionId);
}
}
private static class Decision {
final int id;
boolean allow;
private Decision(int id) {
this.id = id;
}
}
}