use scopes for trust
This commit is contained in:
parent
9c64f9c24c
commit
177320d8fe
|
@ -26,7 +26,6 @@ import im.conversations.android.xmpp.ConnectionPool;
|
||||||
import im.conversations.android.xmpp.ConnectionState;
|
import im.conversations.android.xmpp.ConnectionState;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.manager.TrustManager;
|
import im.conversations.android.xmpp.manager.TrustManager;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -59,26 +58,28 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
||||||
|
|
||||||
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
|
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
|
||||||
private final HashMap<ByteBuffer, Boolean> trustDecisions = new HashMap<>();
|
private final HashMap<TrustManager.ScopeFingerprint, Boolean> trustDecisions = new HashMap<>();
|
||||||
|
|
||||||
private final Function<byte[], ListenableFuture<Boolean>> trustDecisionCallback =
|
private final Function<TrustManager.ScopeFingerprint, ListenableFuture<Boolean>>
|
||||||
fingerprint -> {
|
trustDecisionCallback =
|
||||||
final var decision = this.trustDecisions.get(ByteBuffer.wrap(fingerprint));
|
scopeFingerprint -> {
|
||||||
if (decision != null) {
|
final var decision = this.trustDecisions.get(scopeFingerprint);
|
||||||
LOGGER.info("Using previous trust decision ({})", decision);
|
if (decision != null) {
|
||||||
return Futures.immediateFuture(decision);
|
LOGGER.info("Using previous trust decision ({})", decision);
|
||||||
}
|
return Futures.immediateFuture(decision);
|
||||||
LOGGER.info("Trust decision arrived in UI");
|
}
|
||||||
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
LOGGER.info("Trust decision arrived in UI");
|
||||||
final var trustDecision = new TrustDecision(fingerprint, settableFuture);
|
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
||||||
final var currentOperation = this.currentOperation;
|
final var trustDecision =
|
||||||
if (currentOperation != null) {
|
new TrustDecision(scopeFingerprint, settableFuture);
|
||||||
currentOperation.cancel(false);
|
final var currentOperation = this.currentOperation;
|
||||||
}
|
if (currentOperation != null) {
|
||||||
this.trustDecision.postValue(trustDecision);
|
currentOperation.cancel(false);
|
||||||
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
|
}
|
||||||
return settableFuture;
|
this.trustDecision.postValue(trustDecision);
|
||||||
};
|
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
|
||||||
|
return settableFuture;
|
||||||
|
};
|
||||||
|
|
||||||
private final AccountRepository accountRepository;
|
private final AccountRepository accountRepository;
|
||||||
|
|
||||||
|
@ -190,9 +191,9 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"trying to commit trust for fingerprint {}",
|
"trying to commit trust for fingerprint {}",
|
||||||
TrustManager.fingerprint(trustDecision.fingerprint));
|
TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array()));
|
||||||
// in case the UI interface hook gets called again before this gets written to DB
|
// in case the UI interface hook gets called again before this gets written to DB
|
||||||
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), true);
|
this.trustDecisions.put(trustDecision.scopeFingerprint, true);
|
||||||
if (trustDecision.decision.isDone()) {
|
if (trustDecision.decision.isDone()) {
|
||||||
ConnectionPool.getInstance(getApplication()).reconnect(account);
|
ConnectionPool.getInstance(getApplication()).reconnect(account);
|
||||||
LOGGER.info("it was already done. we should reconnect");
|
LOGGER.info("it was already done. we should reconnect");
|
||||||
|
@ -209,9 +210,9 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Rejecting trust decision for {}",
|
"Rejecting trust decision for {}",
|
||||||
TrustManager.fingerprint(trustDecision.fingerprint));
|
TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array()));
|
||||||
trustDecision.decision.set(false);
|
trustDecision.decision.set(false);
|
||||||
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), false);
|
this.trustDecisions.put(trustDecision.scopeFingerprint, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<String> getFingerprint() {
|
public LiveData<String> getFingerprint() {
|
||||||
|
@ -221,7 +222,7 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
if (td == null) {
|
if (td == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return TrustManager.fingerprint(td.fingerprint, 8);
|
return TrustManager.fingerprint(td.scopeFingerprint.fingerprint.array(), 8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -472,11 +473,12 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TrustDecision {
|
public static class TrustDecision {
|
||||||
public final byte[] fingerprint;
|
public final TrustManager.ScopeFingerprint scopeFingerprint;
|
||||||
public final SettableFuture<Boolean> decision;
|
public final SettableFuture<Boolean> decision;
|
||||||
|
|
||||||
public TrustDecision(byte[] fingerprint, SettableFuture<Boolean> decision) {
|
public TrustDecision(
|
||||||
this.fingerprint = fingerprint;
|
TrustManager.ScopeFingerprint scopeFingerprint, SettableFuture<Boolean> decision) {
|
||||||
|
this.scopeFingerprint = scopeFingerprint;
|
||||||
this.decision = decision;
|
this.decision = decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -510,9 +510,10 @@ public class XmppConnection implements Runnable {
|
||||||
} else {
|
} else {
|
||||||
keyManager = new KeyManager[] {new MyKeyManager(context, credential)};
|
keyManager = new KeyManager[] {new MyKeyManager(context, credential)};
|
||||||
}
|
}
|
||||||
|
final String scope = String.format("xmpp:%s", account.address.getDomain().toString());
|
||||||
sc.init(
|
sc.init(
|
||||||
keyManager,
|
keyManager,
|
||||||
new X509TrustManager[] {getManager(TrustManager.class)},
|
new X509TrustManager[] {getManager(TrustManager.class).scopedTrustManager(scope)},
|
||||||
Conversations.SECURE_RANDOM);
|
Conversations.SECURE_RANDOM);
|
||||||
return sc.getSocketFactory();
|
return sc.getSocketFactory();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package im.conversations.android.xmpp.manager;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.hash.Hashing;
|
import com.google.common.hash.Hashing;
|
||||||
|
@ -11,6 +12,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import im.conversations.android.AppSettings;
|
import im.conversations.android.AppSettings;
|
||||||
import im.conversations.android.tls.TrustManagers;
|
import im.conversations.android.tls.TrustManagers;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
@ -22,62 +24,19 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@SuppressLint("CustomX509TrustManager")
|
@SuppressLint("CustomX509TrustManager")
|
||||||
public class TrustManager extends AbstractManager implements X509TrustManager {
|
public class TrustManager extends AbstractManager {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class);
|
||||||
|
|
||||||
private final AppSettings appSettings;
|
private final AppSettings appSettings;
|
||||||
|
|
||||||
private Function<byte[], ListenableFuture<Boolean>> userInterfaceCallback;
|
private Function<ScopeFingerprint, ListenableFuture<Boolean>> userInterfaceCallback;
|
||||||
|
|
||||||
public TrustManager(final Context context, final XmppConnection connection) {
|
public TrustManager(final Context context, final XmppConnection connection) {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
this.appSettings = new AppSettings(context);
|
this.appSettings = new AppSettings(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkClientTrusted(final X509Certificate[] chain, final String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
throw new CertificateException(
|
|
||||||
"This implementation has no support for client certificates");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
if (chain.length == 0) {
|
|
||||||
throw new CertificateException("Certificate chain is zero length");
|
|
||||||
}
|
|
||||||
for (final X509Certificate certificate : chain) {
|
|
||||||
certificate.checkValidity();
|
|
||||||
}
|
|
||||||
final boolean isTrustSystemCaStore = appSettings.isTrustSystemCAStore();
|
|
||||||
if (isTrustSystemCaStore && isServerTrustedInSystemCaStore(chain, authType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final X509Certificate certificate = chain[0];
|
|
||||||
final byte[] fingerprint = Hashing.sha256().hashBytes(certificate.getEncoded()).asBytes();
|
|
||||||
LOGGER.info("Looking up {} in database", fingerprint(fingerprint));
|
|
||||||
final var callback = this.userInterfaceCallback;
|
|
||||||
if (callback == null) {
|
|
||||||
throw new CertificateException(
|
|
||||||
"No user interface registered. Can not trust certificate");
|
|
||||||
}
|
|
||||||
final ListenableFuture<Boolean> futureDecision = callback.apply(fingerprint);
|
|
||||||
final boolean decision;
|
|
||||||
try {
|
|
||||||
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
|
|
||||||
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
|
|
||||||
futureDecision.cancel(true);
|
|
||||||
throw new CertificateException(
|
|
||||||
"Timeout waiting for user response", Throwables.getRootCause(e));
|
|
||||||
}
|
|
||||||
if (decision) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new CertificateException("User did not trust certificate");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isServerTrustedInSystemCaStore(
|
private boolean isServerTrustedInSystemCaStore(
|
||||||
final X509Certificate[] chain, final String authType) {
|
final X509Certificate[] chain, final String authType) {
|
||||||
final var trustManager = TrustManagers.getTrustManager();
|
final var trustManager = TrustManagers.getTrustManager();
|
||||||
|
@ -92,11 +51,6 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return new X509Certificate[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String fingerprint(final byte[] bytes) {
|
public static String fingerprint(final byte[] bytes) {
|
||||||
return fingerprint(bytes, bytes.length);
|
return fingerprint(bytes, bytes.length);
|
||||||
}
|
}
|
||||||
|
@ -112,15 +66,105 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserInterfaceCallback(
|
public void setUserInterfaceCallback(
|
||||||
final Function<byte[], ListenableFuture<Boolean>> callback) {
|
final Function<ScopeFingerprint, ListenableFuture<Boolean>> callback) {
|
||||||
this.userInterfaceCallback = callback;
|
this.userInterfaceCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeUserInterfaceCallback(
|
public void removeUserInterfaceCallback(
|
||||||
final Function<byte[], ListenableFuture<Boolean>> callback) {
|
final Function<ScopeFingerprint, ListenableFuture<Boolean>> callback) {
|
||||||
if (this.userInterfaceCallback == callback) {
|
if (this.userInterfaceCallback == callback) {
|
||||||
LOGGER.info("Remove user interface callback");
|
LOGGER.info("Remove user interface callback");
|
||||||
this.userInterfaceCallback = null;
|
this.userInterfaceCallback = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public X509TrustManager scopedTrustManager(final String scope) {
|
||||||
|
return new ScopedTrustManager(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ScopeFingerprint {
|
||||||
|
public final String scope;
|
||||||
|
public final ByteBuffer fingerprint;
|
||||||
|
|
||||||
|
public ScopeFingerprint(final String scope, final byte[] fingerprint) {
|
||||||
|
this(scope, ByteBuffer.wrap(fingerprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScopeFingerprint(final String scope, final ByteBuffer fingerprint) {
|
||||||
|
this.scope = scope;
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ScopeFingerprint that = (ScopeFingerprint) o;
|
||||||
|
return Objects.equal(scope, that.scope) && Objects.equal(fingerprint, that.fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(scope, fingerprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScopedTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
|
private final String scope;
|
||||||
|
|
||||||
|
private ScopedTrustManager(final String scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(final X509Certificate[] chain, final String authType)
|
||||||
|
throws CertificateException {
|
||||||
|
throw new CertificateException(
|
||||||
|
"This implementation has no support for client certificates");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
|
||||||
|
throws CertificateException {
|
||||||
|
if (chain.length == 0) {
|
||||||
|
throw new CertificateException("Certificate chain is zero length");
|
||||||
|
}
|
||||||
|
for (final X509Certificate certificate : chain) {
|
||||||
|
certificate.checkValidity();
|
||||||
|
}
|
||||||
|
final boolean isTrustSystemCaStore = appSettings.isTrustSystemCAStore();
|
||||||
|
if (isTrustSystemCaStore && isServerTrustedInSystemCaStore(chain, authType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final X509Certificate certificate = chain[0];
|
||||||
|
final byte[] fingerprint =
|
||||||
|
Hashing.sha256().hashBytes(certificate.getEncoded()).asBytes();
|
||||||
|
final var scopeFingerprint = new ScopeFingerprint(scope, fingerprint);
|
||||||
|
LOGGER.info("Looking up {} in database", fingerprint(fingerprint));
|
||||||
|
final var callback = TrustManager.this.userInterfaceCallback;
|
||||||
|
if (callback == null) {
|
||||||
|
throw new CertificateException(
|
||||||
|
"No user interface registered. Can not trust certificate");
|
||||||
|
}
|
||||||
|
final ListenableFuture<Boolean> futureDecision = callback.apply(scopeFingerprint);
|
||||||
|
final boolean decision;
|
||||||
|
try {
|
||||||
|
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
|
||||||
|
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
|
||||||
|
futureDecision.cancel(true);
|
||||||
|
throw new CertificateException(
|
||||||
|
"Timeout waiting for user response", Throwables.getRootCause(e));
|
||||||
|
}
|
||||||
|
if (decision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new CertificateException("User did not trust certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue