upgrade dependencies + finalize new minidns + Verify DANE and display when verified

This commit is contained in:
12aw 2023-11-26 14:23:56 +01:00
parent 6335fe4bd2
commit 0402be5280
9 changed files with 186 additions and 272 deletions

View file

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0-rc02'
classpath 'com.android.tools.build:gradle:8.2.0-rc03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10"
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.14.0"
classpath 'com.novoda:bintray-release:0.8.0'
@ -75,7 +75,7 @@ dependencies {
implementation 'org.whispersystems:signal-protocol-android:2.6.2'
implementation 'com.makeramen:roundedimageview:2.3.0'
implementation 'jetty:javax.servlet:5.1.12'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
@ -107,7 +107,7 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.guava:guava:32.1.3-android'
implementation 'com.github.AppIntro:AppIntro:6.2.0'
implementation 'androidx.browser:browser:1.6.0'
implementation 'androidx.browser:browser:1.7.0'
implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy
implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4'
implementation project(':libs:AXML')
@ -130,7 +130,7 @@ dependencies {
implementation 'com.nineoldandroids:library:2.4.0'
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.emoji2:emoji2-emojipicker:1.4.0"
implementation "androidx.compose.material3:material3-android:1.2.0-alpha10"
implementation "androidx.compose.material3:material3-android:1.2.0-alpha11"
}
ext {

View file

@ -40,10 +40,10 @@ import android.util.Log;
import android.util.SparseArray;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.util.Consumer;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
@ -51,6 +51,8 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.minidns.dane.DaneVerifier;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -81,8 +83,6 @@ import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.BundledTrustManager;
import eu.siacs.conversations.crypto.CombiningTrustManager;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.http.HttpConnectionManager;
@ -117,15 +117,16 @@ public class MemorizingTrustManager {
static String KEYSTORE_DIR = "KeyStore";
static String KEYSTORE_FILE = "KeyStore.bks";
private static int decisionId = 0;
private static SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
private static final SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
Context master;
AppCompatActivity foregroundAct;
NotificationManager notificationManager;
Handler masterHandler;
private File keyStoreFile;
private KeyStore appKeyStore;
private X509TrustManager defaultTrustManager;
private final X509TrustManager defaultTrustManager;
private X509TrustManager appTrustManager;
private final DaneVerifier daneVerifier;
private String poshCacheDir;
/**
@ -139,13 +140,14 @@ public class MemorizingTrustManager {
* The context is used for file management, to display the dialog /
* notification and for obtaining translated strings.
*
* @param context Context for the application.
* @param m Context for the application.
* @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
*/
public MemorizingTrustManager(final Context context, final X509TrustManager defaultTrustManager) {
init(context);
public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
init(m);
this.appTrustManager = getTrustManager(appKeyStore);
this.defaultTrustManager = defaultTrustManager;
this.daneVerifier = new DaneVerifier();
}
/**
@ -159,23 +161,13 @@ public class MemorizingTrustManager {
* The context is used for file management, to display the dialog /
* notification and for obtaining translated strings.
*
* @param context Context for the application.
* @param m Context for the application.
*/
public MemorizingTrustManager(final Context context) {
init(context);
public MemorizingTrustManager(Context m) {
init(m);
this.appTrustManager = getTrustManager(appKeyStore);
try {
final BundledTrustManager bundleTrustManager =
BundledTrustManager.builder()
.loadKeyStore(
context.getResources()
.openRawResource(R.raw.letsencrypt_with_intermediates),
"letsencrypt")
.build();
this.defaultTrustManager = CombiningTrustManager.combineWithDefault(bundleTrustManager);
} catch (final NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) {
throw new RuntimeException(e);
}
this.defaultTrustManager = getTrustManager(null);
this.daneVerifier = new DaneVerifier();
}
private static boolean isIp(final String server) {
@ -234,18 +226,18 @@ public class MemorizingTrustManager {
}
}
void init(final Context context) {
master = context;
masterHandler = new Handler(context.getMainLooper());
void init(final Context m) {
master = m;
masterHandler = new Handler(m.getMainLooper());
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
Application app;
if (context instanceof Application) {
app = (Application) context;
} else if (context instanceof Service) {
app = ((Service) context).getApplication();
} else if (context instanceof AppCompatActivity) {
app = ((AppCompatActivity) context).getApplication();
if (m instanceof Application) {
app = (Application) m;
} else if (m instanceof Service) {
app = ((Service) m).getApplication();
} else if (m instanceof AppCompatActivity) {
app = ((AppCompatActivity) m).getApplication();
} else
throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
@ -289,21 +281,20 @@ public class MemorizingTrustManager {
keyStoreUpdated();
}
private X509TrustManager getTrustManager(final KeyStore keyStore) {
Preconditions.checkNotNull(keyStore);
X509TrustManager getTrustManager(KeyStore ks) {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(keyStore);
tmf.init(ks);
for (TrustManager t : tmf.getTrustManagers()) {
if (t instanceof X509TrustManager) {
return (X509TrustManager) t;
}
}
} catch (final Exception e) {
} catch (Exception e) {
// Here, we are covering up errors. It might be more useful
// however to throw them out of the constructor so the
// embedding app knows something went wrong.
LOGGER.log(Level.SEVERE, "getTrustManager(" + keyStore + ")", e);
LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e);
}
return null;
}
@ -377,14 +368,20 @@ public class MemorizingTrustManager {
}
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive, String verifiedHostname, int port, Consumer<Boolean> daneCb)
throws CertificateException {
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
try {
LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
if (isServer)
if (isServer) {
if (verifiedHostname != null) {
if (daneVerifier.verifyCertificateChain(chain, verifiedHostname, port)) {
if (daneCb != null) daneCb.accept(true);
return;
}
}
appTrustManager.checkServerTrusted(chain, authType);
else
} else
appTrustManager.checkClientTrusted(chain, authType);
} catch (final CertificateException ae) {
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
@ -403,7 +400,7 @@ public class MemorizingTrustManager {
} catch (final CertificateException e) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false);
if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion") && !domain.endsWith(".i2p")) {
if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) {
final String hash = getBase64Hash(chain[0], "SHA-256");
final List<String> fingerprints = getPoshFingerprints(domain);
if (hash != null && fingerprints.size() > 0) {
@ -448,7 +445,7 @@ public class MemorizingTrustManager {
try {
final List<String> results = new ArrayList<>();
final InputStream inputStream = HttpConnectionManager.open(url, useTor, useI2P);
final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8));
final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8));
final JSONObject jsonObject = new JSONObject(body);
int expires = jsonObject.getInt("expires");
if (expires <= 0) {
@ -652,39 +649,45 @@ public class MemorizingTrustManager {
}
}
public X509TrustManager getNonInteractive(String domain) {
return new NonInteractiveMemorizingTrustManager(domain);
public X509TrustManager getNonInteractive(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
return new NonInteractiveMemorizingTrustManager(domain, verifiedHostname, port, daneCb);
}
public X509TrustManager getInteractive(String domain) {
return new InteractiveMemorizingTrustManager(domain);
public X509TrustManager getInteractive(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
return new InteractiveMemorizingTrustManager(domain, verifiedHostname, port, daneCb);
}
public X509TrustManager getNonInteractive() {
return new NonInteractiveMemorizingTrustManager(null);
return new NonInteractiveMemorizingTrustManager(null, null, 0, null);
}
public X509TrustManager getInteractive() {
return new InteractiveMemorizingTrustManager(null);
return new InteractiveMemorizingTrustManager(null, null, 0, null);
}
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
private final String verifiedHostname;
private final int port;
private final Consumer<Boolean> daneCb;
public NonInteractiveMemorizingTrustManager(String domain) {
public NonInteractiveMemorizingTrustManager(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
this.domain = domain;
this.verifiedHostname = verifiedHostname;
this.port = port;
this.daneCb = daneCb;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false, verifiedHostname, port, daneCb);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false, verifiedHostname, port, daneCb);
}
@Override
@ -696,20 +699,26 @@ public class MemorizingTrustManager {
private class InteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
private final String verifiedHostname;
private final int port;
private final Consumer<Boolean> daneCb;
public InteractiveMemorizingTrustManager(String domain) {
public InteractiveMemorizingTrustManager(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
this.domain = domain;
this.verifiedHostname = verifiedHostname;
this.port = port;
this.daneCb = daneCb;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true, verifiedHostname, port, daneCb);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true, verifiedHostname, port, daneCb);
}
@Override

View file

@ -3100,7 +3100,7 @@ public class XmppConnectionService extends Service {
callback.onAccountCreated(account);
if (Config.X509_VERIFICATION) {
try {
getMemorizingTrustManager().getNonInteractive(account.getServer()).checkClientTrusted(chain, "RSA");
getMemorizingTrustManager().getNonInteractive(account.getServer(), null, 0, null).checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
callback.informUser(R.string.certificate_chain_is_not_trusted);
}

View file

@ -1509,8 +1509,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
}
this.binding.verificationBox.setVisibility(View.VISIBLE);
if (mAccount.getXmppConnection() != null && mAccount.getXmppConnection().resolverAuthenticated()) {
this.binding.verificationMessage.setText("DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield);
if (mAccount.getXmppConnection().daneVerified()) {
this.binding.verificationMessage.setText("DNSSEC + DANE Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield_verified);
} else {
this.binding.verificationMessage.setText("DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield);
}
} else {
this.binding.verificationMessage.setText("Not DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield_question);

View file

@ -73,10 +73,19 @@ public class AccountAdapter extends ArrayAdapter<Account> {
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
break;
}
if (account.getXmppConnection() != null && account.getXmppConnection().resolverAuthenticated()) {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield);
if (account.isOnlineAndConnected()) {
viewHolder.binding.verificationIndicator.setVisibility(View.VISIBLE);
if (account.getXmppConnection() != null && account.getXmppConnection().resolverAuthenticated()) {
if (account.getXmppConnection().daneVerified()) {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield_verified);
} else {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield);
}
} else {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield_question);
}
} else {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield_question);
viewHolder.binding.verificationIndicator.setVisibility(View.GONE);
}
final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener(null);

View file

@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
@ -22,15 +23,15 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
//import de.gultsch.minidns.AndroidDnsClient;
//import de.gultsch.minidns.AndroidDNSClient;
import org.minidns.AbstractDnsClient;
import org.minidns.DnsCache;
import org.minidns.DnsClient;
import org.minidns.DnsName.DnsName;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsmessage.Question;
import org.minidns.record.Record;
import org.minidns.cache.LruCache;
import org.minidns.dnssec.DnssecResultNotAuthenticException;
import org.minidns.dnsserverlookup.AndroidUsingExec;
import org.minidns.hla.DnssecResolverApi;
import org.minidns.hla.ResolverApi;
@ -112,7 +113,7 @@ public class Resolver {
}
public static List<Result> resolve(final String domain) {
final List<Result> ipResults = (List<Result>) fromIpAddress(domain, DEFAULT_PORT_XMPP);
final List<Result> ipResults = fromIpAddress(domain, DEFAULT_PORT_XMPP);
if (ipResults.size() > 0) {
return ipResults;
}
@ -178,7 +179,7 @@ public class Resolver {
}
}
private static List<Result> fromIpAddress(String domain, final int defaultPortXmpp) {
private static List<Result> fromIpAddress(String domain, int port) {
if (!IP.matches(domain)) {
return Collections.emptyList();
}
@ -193,52 +194,31 @@ public class Resolver {
}
}
private static List<Result> resolveSrv(final String domain, final boolean directTls) throws IOException {
final DnsName DnsName = DnsName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
final ResolverResult<SRV> result = resolveWithFallback(DnsName, SRV.class);
private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
DnsName dnsName = DnsName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
final List<Result> results = new ArrayList<>();
final List<Thread> threads = new ArrayList<>();
final List<Result> fallbackResults = new ArrayList<>();
final List<Thread> fallbackThreads = new ArrayList<>();
for (SRV record : result.getAnswersOrEmptySet()) {
if (record.name.length() == 0 && record.priority == 0) {
continue;
}
threads.add(new Thread(() -> {
final List<Result> ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls);
synchronized (results) {
results.addAll(ipv6s);
}
}));
threads.add(new Thread(() -> {
final List<Result> ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls);
if (ipv4s.size() == 0) {
Result resolverResult = Result.fromRecord(record, directTls, true);
Result resolverResult = Result.fromRecord(record, directTls);
resolverResult.authenticated = result.isAuthenticData();
ipv4s.add(resolverResult);
}
synchronized (results) {
results.addAll(ipv4s);
}
}));
fallbackThreads.add(new Thread(() -> {
try {
ResolverResult<CNAME> cnames = resolveWithFallback(record.name, CNAME.class, result.isAuthenticData());
for (CNAME cname : cnames.getAnswersOrEmptySet()) {
final List<Result> ipv6s = resolveIp(record, cname.name, AAAA.class, cnames.isAuthenticData(), directTls);
synchronized (fallbackResults) {
fallbackResults.addAll(ipv6s);
}
final List<Result> ipv4s = resolveIp(record, cname.name, A.class, cnames.isAuthenticData(), directTls);
synchronized (fallbackResults) {
fallbackResults.addAll(ipv4s);
}
}
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + " cname in srv (against RFC2782) - run slow fallback");
} catch (Throwable throwable) {
Log.i(Config.LOGTAG, Resolver.class.getSimpleName() + " error resolving srv cname-fallback records", throwable);
threads.add(new Thread(() -> {
final List<Result> ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls);
synchronized (results) {
results.addAll(ipv6s);
}
}));
}
@ -249,37 +229,18 @@ public class Resolver {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
return Collections.emptyList();
}
}
if (results.size() > 0) {
return results;
}
for (Thread thread : fallbackThreads) {
thread.start();
}
for (Thread thread : fallbackThreads) {
try {
thread.join();
} catch (InterruptedException e) {
return Collections.emptyList();
}
}
return fallbackResults;
return results;
}
private static <D extends InternetAddressRR> List<Result> resolveIp(final SRV srv, final Class<D> type, final boolean authenticated, final boolean directTls) {
return resolveIp(srv, srv.name, type, authenticated, directTls);
}
private static <D extends InternetAddressRR> List<Result> resolveIp(final SRV srv, final DnsName hostname, final Class<D> type, final boolean authenticated, final boolean directTls) {
final List<Result> list = new ArrayList<>();
private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
List<Result> list = new ArrayList<>();
try {
ResolverResult<D> results = resolveWithFallback(hostname, type);
ResolverResult<D> results = resolveWithFallback(srv.name, type);
for (D record : results.getAnswersOrEmptySet()) {
boolean ipv4 = type == A.class;
Result resolverResult = Result.fromRecord(srv, directTls, ipv4);
Result resolverResult = Result.fromRecord(srv, directTls);
resolverResult.authenticated = results.isAuthenticData() && authenticated;
resolverResult.ip = record.getInetAddress();
list.add(resolverResult);
@ -290,26 +251,26 @@ public class Resolver {
return list;
}
private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
private static List<Result> resolveNoSrvRecords(DnsName dnsName, int port, boolean withCnames) {
List<Result> results = new ArrayList<>();
try {
ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
Log.d("WUTr", "" + aResult.isAuthenticData() + " " + aResult.getAnswersOrEmptySet());
for (A a : aResult.getAnswersOrEmptySet()) {
Result r = Result.createDefault(dnsName, a.getInetAddress());
Result r = Result.createDefault(dnsName, a.getInetAddress(), port);
r.authenticated = aResult.isAuthenticData();
results.add(r);
}
ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
Result r = Result.createDefault(dnsName, aaaa.getInetAddress(), port);
r.authenticated = aaaaResult.isAuthenticData();
results.add(r);
}
if (results.size() == 0 && withCnames) {
ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
for (Result r : resolveNoSrvRecords(cname.name, false)) {
for (Result r : resolveNoSrvRecords(cname.name, port, false)) {
r.authenticated = r.authenticated && cnameResult.isAuthenticData();
results.add(r);
}
@ -320,7 +281,7 @@ public class Resolver {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
}
}
results.add(Result.createDefault(dnsName));
results.add(Result.createDefault(dnsName, port));
return results;
}
@ -336,55 +297,7 @@ public class Resolver {
return ResolverApi.INSTANCE.resolve(question);
}
private static Result happyEyeball(final List<Result> r) {
final String logID = Long.toHexString(Double.doubleToLongBits(Math.random()));
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") with " + r.toString());
if (r.size() == 0) return null;
Result result;
if (r.size() == 1 && r.get(0).ip != null) {
result = r.get(0);
result.setLogID(logID);
result.connect();
return result;
}
for (Result res : r) {
res.setLogID(logID);
}
final ExecutorService executor = Executors.newFixedThreadPool(4);
try {
result = executor.invokeAny(r);
executor.shutdown();
Thread disconnector = new Thread(() -> {
while (true) {
try {
if (executor.awaitTermination(5, TimeUnit.SECONDS)) break;
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") wait for cleanup ...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.i(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") cleanup");
for (Result re : r) {
if (!re.equals(result)) re.disconnect();
}
});
disconnector.start();
Log.i(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") used: " + result.toString());
return result;
} catch (InterruptedException e) {
Log.e(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") failed: ", e);
return null;
} catch (ExecutionException e) {
Log.i(Config.LOGTAG, Resolver.class.getSimpleName() + ": happy eyeball (" + logID + ") unable to connect to one address");
return null;
}
}
public static class Result implements Comparable<Result>, Callable<Result> {
public static class Result implements Comparable<Result> {
public static final String DOMAIN = "domain";
public static final String IP = "ip";
public static final String HOSTNAME = "hostname";
@ -405,43 +318,29 @@ public class Resolver {
private String logID = "";
static Result fromRecord(final SRV srv, final boolean directTls, final boolean ipv4) {
static Result fromRecord(SRV srv, boolean directTls) {
Result result = new Result();
result.timeRequested = System.currentTimeMillis();
result.port = srv.port;
result.hostname = srv.name;
if (ipv4) {
try {
result.ip = InetAddress.getByName(result.hostname.toString());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
result.directTls = directTls;
result.priority = srv.priority;
return result;
}
static Result createDefault(final DnsName hostname, final int port) {
InetAddress ip = null;
try {
ip = InetAddress.getByName(hostname.toString());
} catch (UnknownHostException e) {
e.printStackTrace();
}
return createDefault(hostname, ip, port);
}
static Result createDefault(final DnsName hostname, final InetAddress ip, final int port) {
static Result createDefault(DnsName hostname, InetAddress ip, int port) {
Result result = new Result();
result.timeRequested = System.currentTimeMillis();
result.port = port;
result.directTls = useDirectTls(port);
result.hostname = hostname;
result.ip = ip;
result.directTls = useDirectTls(port);
return result;
}
static Result createDefault(DnsName hostname, int port) {
return createDefault(hostname, null, port);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -468,6 +367,14 @@ public class Resolver {
return result;
}
public InetAddress getIp() {
return ip;
}
public int getPort() {
return port;
}
public DnsName getHostname() {
return hostname;
}
@ -501,52 +408,21 @@ public class Resolver {
'}';
}
public void connect() {
if (this.socket != null) {
this.disconnect();
}
if (this.ip == null || this.port == 0) {
Log.d(Config.LOGTAG, "Resolver did not get IP:port (" + this.ip + ":" + this.port + ")");
return;
}
final InetSocketAddress addr = new InetSocketAddress(this.ip, this.port);
this.socket = new Socket();
try {
long time = System.currentTimeMillis();
this.socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
time = System.currentTimeMillis() - time;
if (this.logID != null && !this.logID.isEmpty()) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": Result (" + this.logID + ") connect: " + toString() + " after: " + time + " ms");
} else {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": Result connect: " + toString() + " after: " + time + " ms");
}
} catch (IOException e) {
e.printStackTrace();
this.disconnect();
}
}
public void disconnect() {
if (this.socket != null) {
FileBackend.close(this.socket);
this.socket = null;
if (this.logID != null && !this.logID.isEmpty()) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": Result (" + this.logID + ") disconnect: " + toString());
} else {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": Result disconnect: " + toString());
}
}
}
public void setLogID(final String logID) {
this.logID = logID;
}
@Override
public int compareTo(@NonNull final Result result) {
public int compareTo(@NonNull Result result) {
if (result.priority == priority) {
if (directTls == result.directTls) {
return 0;
if (ip == null && result.ip == null) {
return 0;
} else if (ip != null && result.ip != null) {
if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
return 0;
} else {
return ip instanceof Inet4Address ? -1 : 1;
}
} else {
return ip != null ? -1 : 1;
}
} else {
return directTls ? -1 : 1;
}
@ -555,14 +431,6 @@ public class Resolver {
}
}
@Override
public Result call() throws Exception {
this.connect();
if (this.socket != null && this.socket.isConnected()) {
return this;
}
throw new Exception("Resolver.Result was not possible to connect - should be catched by executor");
}
public static Result fromCursor(Cursor cursor) {
final Result result = new Result();

View file

@ -16,6 +16,7 @@ import com.google.common.base.Optional;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.google.common.base.Strings;
@ -34,6 +35,7 @@ import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
@ -59,6 +61,7 @@ import java.util.regex.Matcher;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509KeyManager;
@ -211,6 +214,7 @@ public class XmppConnection implements Runnable {
private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch;
private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
private boolean dane = false;
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
@ -245,6 +249,9 @@ public class XmppConnection implements Runnable {
}
}
public boolean daneVerified() {
return dane;
}
public boolean resolverAuthenticated() {
if (currentResolverResult == null) return false;
@ -316,6 +323,7 @@ public class XmppConnection implements Runnable {
this.isBound = false;
this.attempt++;
this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified
this.dane = false;
// with dnssec
try {
Socket localSocket;
@ -417,7 +425,7 @@ public class XmppConnection implements Runnable {
}
} else {
final String domain = account.getServer();
final List<Resolver.Result> results;
List<Resolver.Result> results;
final boolean hardcoded = extended && !account.getHostname().isEmpty();
if (hardcoded) {
results = Resolver.fromHardCoded(account.getHostname(), account.getPort());
@ -458,7 +466,7 @@ public class XmppConnection implements Runnable {
iterator.hasNext(); ) {
final Resolver.Result result = iterator.next();
if (results == null || results.getSocket() == null) {
if (results == null || result.getSocket() == null) {
results = Resolver.resolve(domain);
}
if (results == null) {
@ -472,12 +480,12 @@ public class XmppConnection implements Runnable {
}
try {
// if tls is true, encryption is implied and must not be started
features.encryptionEnabled = results.isDirectTls();
verifiedHostname = results.isAuthenticated() ? results.getHostname().toString() : null;
features.encryptionEnabled = result.isDirectTls();
verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null;
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
+ ": using values from resolver " + results.toString());
localSocket = results.getSocket();
localSocket = result.getSocket();
if (features.encryptionEnabled) {
localSocket = upgradeSocketToTls(localSocket);
@ -488,13 +496,13 @@ public class XmppConnection implements Runnable {
localSocket.setSoTimeout(
0); // reset to 0; once the connection is established we dont
// want this
if (!hardcoded && !results.equals(storedBackupResult)) {
if (!hardcoded && !result.equals(storedBackupResult)) {
mXmppConnectionService.databaseBackend.saveResolverResult(
domain, results);
domain, result);
}
this.currentResolverResult = result;
this.seeOtherHostResolverResult = null;
// successfully connected to server that speaks xmpp
break; // successfully connected to server that speaks xmpp
} else {
FileBackend.close(localSocket);
throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
@ -592,7 +600,7 @@ public class XmppConnection implements Runnable {
return success;
}
private SSLSocketFactory getSSLSocketFactory()
private SSLSocketFactory getSSLSocketFactory(int port, Consumer<Boolean> daneCb)
throws NoSuchAlgorithmException, KeyManagementException {
final SSLContext sc = SSLSockets.getSSLContext();
final MemorizingTrustManager trustManager =
@ -608,8 +616,8 @@ public class XmppConnection implements Runnable {
keyManager,
new X509TrustManager[] {
mInteractive
? trustManager.getInteractive(domain)
: trustManager.getNonInteractive(domain)
? trustManager.getInteractive(domain, verifiedHostname, port, daneCb)
: trustManager.getNonInteractive(domain, verifiedHostname, port, daneCb)
},
SECURE_RANDOM);
return sc.getSocketFactory();
@ -1358,29 +1366,35 @@ public class XmppConnection implements Runnable {
sslSocket.close();
}
private X509Certificate[] certificates(final SSLSession session) throws SSLPeerUnverifiedException {
List<X509Certificate> certs = new ArrayList<>();
for (Certificate certificate : session.getPeerCertificates()) {
if (certificate instanceof X509Certificate) {
certs.add((X509Certificate) certificate);
}
}
return certs.toArray(new X509Certificate[certs.size()]);
}
private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
this.dane = false;
final SSLSocketFactory sslSocketFactory;
try {
sslSocketFactory = getSSLSocketFactory();
sslSocketFactory = getSSLSocketFactory(socket.getPort(), (d) -> this.dane = d);
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
throw new StateChangingException(Account.State.TLS_ERROR);
}
final InetAddress address = socket.getInetAddress();
final SSLSocket sslSocket;
try {
sslSocket =
(SSLSocket)
sslSocketFactory.createSocket(
socket, address.getHostAddress(), socket.getPort(), true);
} catch (Exception e) {
throw new StateChangingException(Account.State.TLS_ERROR);
}
final SSLSocket sslSocket =
(SSLSocket)
sslSocketFactory.createSocket(
socket, address.getHostAddress(), socket.getPort(), true);
SSLSockets.setSecurity(sslSocket);
SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
try {
if (!xmppDomainVerifier.verify(
if (!dane && !xmppDomainVerifier.verify(
account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
Log.d(
Config.LOGTAG,

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M438,622L664,396L607,339L438,508L354,424L297,481L438,622ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -42,7 +42,6 @@
<bool name="use_inner_storage">true</bool>
<string name="invidious_host">monocles.live</string>
<bool name="plain_text_logs">false</bool>
<bool name="validate_hostname">false</bool>
<bool name="show_foreground_service">true</bool>
<bool name="warn_unencrypted_chat">true</bool>
<bool name="hide_you_are_not_participating">false</bool>