put trust manager framework in place

This commit is contained in:
Daniel Gultsch 2023-03-01 18:02:14 +01:00
parent be6f4300da
commit 786a6c4c2a
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
6 changed files with 159 additions and 8 deletions

View file

@ -11,6 +11,8 @@ public class AppSettings {
public static final String PREFERENCE_KEY_RINGTONE = "call_ringtone"; public static final String PREFERENCE_KEY_RINGTONE = "call_ringtone";
public static final String PREFERENCE_KEY_BTBV = "btbv"; public static final String PREFERENCE_KEY_BTBV = "btbv";
public static final String PREFERENCE_KEY_TRUST_SYSTEM_CA_STORE = "trust_system_ca_store";
private final Context context; private final Context context;
public AppSettings(final Context context) { public AppSettings(final Context context) {
@ -42,4 +44,12 @@ public class AppSettings {
return sharedPreferences.getBoolean( return sharedPreferences.getBoolean(
PREFERENCE_KEY_BTBV, context.getResources().getBoolean(R.bool.btbv)); PREFERENCE_KEY_BTBV, context.getResources().getBoolean(R.bool.btbv));
} }
public boolean isTrustSystemCAStore() {
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
return sharedPreferences.getBoolean(
PREFERENCE_KEY_TRUST_SYSTEM_CA_STORE,
context.getResources().getBoolean(R.bool.btbv));
}
} }

View file

@ -7,12 +7,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Optional;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.Connection; import im.conversations.android.database.model.Connection;
@ -20,10 +22,13 @@ import im.conversations.android.repository.AccountRepository;
import im.conversations.android.ui.Event; import im.conversations.android.ui.Event;
import im.conversations.android.util.ConnectionStates; import im.conversations.android.util.ConnectionStates;
import im.conversations.android.xmpp.ConnectionException; import im.conversations.android.xmpp.ConnectionException;
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 java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
@ -50,6 +55,17 @@ 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 Function<byte[], ListenableFuture<Boolean>> trustDecisionCallback =
fingerprint -> {
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
final var trustDecision = new TrustDecision(fingerprint, settableFuture);
LOGGER.debug("posting trust decision");
this.trustDecision.postValue(trustDecision);
return settableFuture;
};
private final AccountRepository accountRepository; private final AccountRepository accountRepository;
private Account account; private Account account;
@ -134,6 +150,7 @@ public class SetupViewModel extends AndroidViewModel {
decideNextStep(Target.ENTER_ADDRESS, account); decideNextStep(Target.ENTER_ADDRESS, account);
return true; return true;
} else { } else {
this.unregisterTrustDecisionCallback();
this.account = null; this.account = null;
// when the XMPP address changes we want to reset connection info too // when the XMPP address changes we want to reset connection info too
@ -187,9 +204,34 @@ public class SetupViewModel extends AndroidViewModel {
private void setAccount(@NonNull final Account account) { private void setAccount(@NonNull final Account account) {
this.account = account; this.account = account;
this.registerTrustDecisionCallback();
this.decideNextStep(Target.ENTER_ADDRESS, account); this.decideNextStep(Target.ENTER_ADDRESS, account);
} }
private Optional<TrustManager> getTrustManager() {
final var account = this.account;
if (account == null) {
return Optional.absent();
}
return ConnectionPool.getInstance(getApplication())
.get(account)
.transform(xc -> xc.getManager(TrustManager.class));
}
private void registerTrustDecisionCallback() {
final var optionalTrustManager = getTrustManager();
if (optionalTrustManager.isPresent()) {
optionalTrustManager.get().setUserInterfaceCallback(this.trustDecisionCallback);
}
}
private void unregisterTrustDecisionCallback() {
final var optionalTrustManager = getTrustManager();
if (optionalTrustManager.isPresent()) {
optionalTrustManager.get().removeUserInterfaceCallback(this.trustDecisionCallback);
}
}
private void decideNextStep(final Target current, @NonNull final Account account) { private void decideNextStep(final Target current, @NonNull final Account account) {
final ListenableFuture<XmppConnection> connectedFuture = final ListenableFuture<XmppConnection> connectedFuture =
this.accountRepository.getConnectedFuture(account); this.accountRepository.getConnectedFuture(account);
@ -336,6 +378,7 @@ public class SetupViewModel extends AndroidViewModel {
public void cancelSetup() { public void cancelSetup() {
final var account = this.account; final var account = this.account;
if (account != null) { if (account != null) {
this.unregisterTrustDecisionCallback();
this.account = null; this.account = null;
this.accountRepository.deleteAccountAsync(account); this.accountRepository.deleteAccountAsync(account);
} }
@ -345,10 +388,25 @@ public class SetupViewModel extends AndroidViewModel {
return this.redirection; return this.redirection;
} }
public void onCleared() {
super.onCleared();
this.unregisterTrustDecisionCallback();
}
public enum Target { public enum Target {
ENTER_ADDRESS, ENTER_ADDRESS,
ENTER_PASSWORD, ENTER_PASSWORD,
ENTER_HOSTNAME, ENTER_HOSTNAME,
DONE DONE
} }
public static class TrustDecision {
public final byte[] fingerprint;
public final SettableFuture<Boolean> decision;
public TrustDecision(byte[] fingerprint, SettableFuture<Boolean> decision) {
this.fingerprint = fingerprint;
this.decision = decision;
}
}
} }

View file

@ -28,7 +28,6 @@ import im.conversations.android.database.model.Credential;
import im.conversations.android.dns.Resolver; import im.conversations.android.dns.Resolver;
import im.conversations.android.socks.SocksSocketFactory; import im.conversations.android.socks.SocksSocketFactory;
import im.conversations.android.tls.SSLSockets; import im.conversations.android.tls.SSLSockets;
import im.conversations.android.tls.TrustManagers;
import im.conversations.android.tls.XmppDomainVerifier; import im.conversations.android.tls.XmppDomainVerifier;
import im.conversations.android.util.PendingItem; import im.conversations.android.util.PendingItem;
import im.conversations.android.xml.Element; import im.conversations.android.xml.Element;
@ -39,6 +38,7 @@ import im.conversations.android.xml.XmlReader;
import im.conversations.android.xmpp.manager.AbstractManager; import im.conversations.android.xmpp.manager.AbstractManager;
import im.conversations.android.xmpp.manager.CarbonsManager; import im.conversations.android.xmpp.manager.CarbonsManager;
import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.manager.TrustManager;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.StreamElement; import im.conversations.android.xmpp.model.StreamElement;
import im.conversations.android.xmpp.model.bind2.BindInlineFeatures; import im.conversations.android.xmpp.model.bind2.BindInlineFeatures;
@ -510,13 +510,9 @@ public class XmppConnection implements Runnable {
} else { } else {
keyManager = new KeyManager[] {new MyKeyManager(context, credential)}; keyManager = new KeyManager[] {new MyKeyManager(context, credential)};
} }
final String domain = account.address.getDomain().toString();
// TODO we used to use two different trust managers; interactive and non interactive (to
// trigger SSL cert prompts)
// we need a better solution for this using live data or similar
sc.init( sc.init(
keyManager, keyManager,
new X509TrustManager[] {TrustManagers.getTrustManager()}, new X509TrustManager[] {getManager(TrustManager.class)},
Conversations.SECURE_RANDOM); Conversations.SECURE_RANDOM);
return sc.getSocketFactory(); return sc.getSocketFactory();
} }

View file

@ -1,16 +1,35 @@
package im.conversations.android.xmpp.manager; package im.conversations.android.xmpp.manager;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Bytes;
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.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
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.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressLint("CustomX509TrustManager")
public class TrustManager extends AbstractManager implements X509TrustManager { public class TrustManager extends AbstractManager implements X509TrustManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TrustManager.class);
private final AppSettings appSettings; private final AppSettings appSettings;
private Function<byte[], 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);
@ -18,14 +37,80 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
@Override @Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType) public void checkClientTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {} throws CertificateException {
throw new CertificateException(
"This implementation has no support for client certificates");
}
@Override @Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType) public void checkServerTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {} 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) {
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();
if (trustManager == null) {
return false;
}
try {
trustManager.checkServerTrusted(chain, authType);
return true;
} catch (final CertificateException e) {
return false;
}
}
@Override @Override
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
} }
public static String fingerprint(final byte[] bytes) {
return Joiner.on(':')
.join(Lists.transform(Bytes.asList(bytes), b -> String.format("%02X", b)));
}
public void setUserInterfaceCallback(
final Function<byte[], ListenableFuture<Boolean>> callback) {
this.userInterfaceCallback = callback;
}
public void removeUserInterfaceCallback(
final Function<byte[], ListenableFuture<Boolean>> callback) {
if (this.userInterfaceCallback == callback) {
LOGGER.info("Remove user interface callback");
this.userInterfaceCallback = null;
}
}
} }

View file

@ -5,6 +5,7 @@
<bool name="enter_is_send">false</bool> <bool name="enter_is_send">false</bool>
<bool name="allow_screenshots">true</bool> <bool name="allow_screenshots">true</bool>
<bool name="btbv">true</bool> <bool name="btbv">true</bool>
<bool name="trust_system_ca_store">true</bool>
<string name="omemo_setting_default" translatable="false">default_on</string> <string name="omemo_setting_default" translatable="false">default_on</string>
<string name="incoming_call_ringtone" translatable="false">content://settings/system/ringtone</string> <string name="incoming_call_ringtone" translatable="false">content://settings/system/ringtone</string>
<integer name="grace_period">144</integer> <integer name="grace_period">144</integer>

View file

@ -20,6 +20,7 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/pref_category_server_connection"> <PreferenceCategory android:title="@string/pref_category_server_connection">
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="@bool/trust_system_ca_store"
android:icon="@drawable/ic_assured_workload_24dp" android:icon="@drawable/ic_assured_workload_24dp"
android:key="trust_system_ca_store" android:key="trust_system_ca_store"
android:summary="@string/pref_title_trust_system_ca_store_summary" android:summary="@string/pref_title_trust_system_ca_store_summary"