From 177320d8fef2e685d1ec01dbe90b0af68289f728 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Mar 2023 10:10:12 +0100 Subject: [PATCH] use scopes for trust --- .../android/ui/model/SetupViewModel.java | 58 +++---- .../android/xmpp/XmppConnection.java | 3 +- .../android/xmpp/manager/TrustManager.java | 148 ++++++++++++------ 3 files changed, 128 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java b/app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java index 3f7a21c18..c43a5590b 100644 --- a/app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java +++ b/app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java @@ -26,7 +26,6 @@ import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionState; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.manager.TrustManager; -import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Locale; @@ -59,26 +58,28 @@ public class SetupViewModel extends AndroidViewModel { private final MutableLiveData> redirection = new MutableLiveData<>(); private final MutableLiveData trustDecision = new MutableLiveData<>(); - private final HashMap trustDecisions = new HashMap<>(); + private final HashMap trustDecisions = new HashMap<>(); - private final Function> trustDecisionCallback = - fingerprint -> { - final var decision = this.trustDecisions.get(ByteBuffer.wrap(fingerprint)); - if (decision != null) { - LOGGER.info("Using previous trust decision ({})", decision); - return Futures.immediateFuture(decision); - } - LOGGER.info("Trust decision arrived in UI"); - final SettableFuture settableFuture = SettableFuture.create(); - final var trustDecision = new TrustDecision(fingerprint, settableFuture); - final var currentOperation = this.currentOperation; - if (currentOperation != null) { - currentOperation.cancel(false); - } - this.trustDecision.postValue(trustDecision); - this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE)); - return settableFuture; - }; + private final Function> + trustDecisionCallback = + scopeFingerprint -> { + final var decision = this.trustDecisions.get(scopeFingerprint); + if (decision != null) { + LOGGER.info("Using previous trust decision ({})", decision); + return Futures.immediateFuture(decision); + } + LOGGER.info("Trust decision arrived in UI"); + final SettableFuture settableFuture = SettableFuture.create(); + final var trustDecision = + new TrustDecision(scopeFingerprint, settableFuture); + final var currentOperation = this.currentOperation; + if (currentOperation != null) { + currentOperation.cancel(false); + } + this.trustDecision.postValue(trustDecision); + this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE)); + return settableFuture; + }; private final AccountRepository accountRepository; @@ -190,9 +191,9 @@ public class SetupViewModel extends AndroidViewModel { } LOGGER.info( "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 - this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), true); + this.trustDecisions.put(trustDecision.scopeFingerprint, true); if (trustDecision.decision.isDone()) { ConnectionPool.getInstance(getApplication()).reconnect(account); LOGGER.info("it was already done. we should reconnect"); @@ -209,9 +210,9 @@ public class SetupViewModel extends AndroidViewModel { } LOGGER.info( "Rejecting trust decision for {}", - TrustManager.fingerprint(trustDecision.fingerprint)); + TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array())); trustDecision.decision.set(false); - this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), false); + this.trustDecisions.put(trustDecision.scopeFingerprint, false); } public LiveData getFingerprint() { @@ -221,7 +222,7 @@ public class SetupViewModel extends AndroidViewModel { if (td == null) { return null; } 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 final byte[] fingerprint; + public final TrustManager.ScopeFingerprint scopeFingerprint; public final SettableFuture decision; - public TrustDecision(byte[] fingerprint, SettableFuture decision) { - this.fingerprint = fingerprint; + public TrustDecision( + TrustManager.ScopeFingerprint scopeFingerprint, SettableFuture decision) { + this.scopeFingerprint = scopeFingerprint; this.decision = decision; } } diff --git a/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java index fe8eb8f2d..d21c6c54e 100644 --- a/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -510,9 +510,10 @@ public class XmppConnection implements Runnable { } else { keyManager = new KeyManager[] {new MyKeyManager(context, credential)}; } + final String scope = String.format("xmpp:%s", account.address.getDomain().toString()); sc.init( keyManager, - new X509TrustManager[] {getManager(TrustManager.class)}, + new X509TrustManager[] {getManager(TrustManager.class).scopedTrustManager(scope)}, Conversations.SECURE_RANDOM); return sc.getSocketFactory(); } diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/TrustManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/TrustManager.java index 192c2e1d6..1c5cdf31d 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/TrustManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/TrustManager.java @@ -3,6 +3,7 @@ package im.conversations.android.xmpp.manager; import android.annotation.SuppressLint; import android.content.Context; import com.google.common.base.Joiner; +import com.google.common.base.Objects; import com.google.common.base.Throwables; import com.google.common.collect.Lists; 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.tls.TrustManagers; import im.conversations.android.xmpp.XmppConnection; +import java.nio.ByteBuffer; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.ExecutionException; @@ -22,62 +24,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressLint("CustomX509TrustManager") -public class TrustManager extends AbstractManager implements X509TrustManager { +public class TrustManager extends AbstractManager { private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class); private final AppSettings appSettings; - private Function> userInterfaceCallback; + private Function> userInterfaceCallback; public TrustManager(final Context context, final XmppConnection connection) { super(context, connection); 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 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( final X509Certificate[] chain, final String authType) { 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) { return fingerprint(bytes, bytes.length); } @@ -112,15 +66,105 @@ public class TrustManager extends AbstractManager implements X509TrustManager { } public void setUserInterfaceCallback( - final Function> callback) { + final Function> callback) { this.userInterfaceCallback = callback; } public void removeUserInterfaceCallback( - final Function> callback) { + final Function> callback) { if (this.userInterfaceCallback == callback) { LOGGER.info("Remove user interface callback"); 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 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]; + } + } }