put trust manager framework in place
This commit is contained in:
parent
be6f4300da
commit
786a6c4c2a
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue