diff --git a/app/build.gradle b/app/build.gradle
index 8dc57ce3a..2e5d1457a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -165,7 +165,7 @@ android {
dependencies {
api (project(':client-common-java')) {
- exclude group: 'net.sf.kxml'
+ exclude group: 'xpp3', module: 'xpp3_min'
}
// support libraries
@@ -187,7 +187,12 @@ dependencies {
// network/protocol libraries
implementation "org.igniterealtime.smack:smack-tcp:$smackVersion"
implementation "org.igniterealtime.smack:smack-experimental:$smackVersion"
- implementation "org.igniterealtime.smack:smack-android:$smackVersion"
+ implementation ("org.igniterealtime.smack:smack-android:$smackVersion") {
+ exclude group: 'xpp3', module: 'xpp3_min'
+ }
+ implementation "org.igniterealtime.smack:smack-omemo:$smackVersion"
+ implementation "org.igniterealtime.smack:smack-omemo-signal:$smackVersion"
+ implementation 'org.whispersystems:signal-protocol-java:2.8.1'
implementation 'info.guardianproject.netcipher:netcipher:1.2.1'
implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.segment.backo:backo:1.0.0'
diff --git a/app/libs/README.md b/app/libs/README.md
new file mode 100644
index 000000000..8c119dc93
--- /dev/null
+++ b/app/libs/README.md
@@ -0,0 +1,2 @@
+Temporary place for the libraries I couldn't automatically reference from Gradle.
+This folder will disappear before release.
diff --git a/app/libs/smack-omemo-4.3.4.jar b/app/libs/smack-omemo-4.3.4.jar
new file mode 100644
index 000000000..756ee9b3f
Binary files /dev/null and b/app/libs/smack-omemo-4.3.4.jar differ
diff --git a/app/libs/smack-omemo-signal-4.3.4.jar b/app/libs/smack-omemo-signal-4.3.4.jar
new file mode 100644
index 000000000..0c69535a0
Binary files /dev/null and b/app/libs/smack-omemo-signal-4.3.4.jar differ
diff --git a/app/proguard.cfg b/app/proguard.cfg
index fb2e2e407..0522f2894 100644
--- a/app/proguard.cfg
+++ b/app/proguard.cfg
@@ -50,7 +50,10 @@
-keep class org.bouncycastle.openpgp.** { *; }
# Smack Core classes should be figured out by Proguard
+# FIXME not really!
-keep class org.jivesoftware.smack.initializer.** { *; }
+-keep class org.jivesoftware.smack.packet.** { *; }
+-keep class org.jivesoftware.smack.**.packet.** { *; }
# keep Smack IM
-keep class org.jivesoftware.smack.im.** { *; }
@@ -82,6 +85,9 @@
-keep class org.jivesoftware.smackx.vcardtemp.** { *; }
-keep class org.jivesoftware.smackx.xdata.** { *; }
-keep class org.jivesoftware.smackx.forward.** { *; }
+-keep class org.jivesoftware.smackx.pubsub.** { *; }
+-keep class org.jivesoftware.smackx.pep.** { *; }
+-keep class org.jivesoftware.smackx.omemo.** { *; }
# keep other Smack utilities
-keep class org.jivesoftware.smack.**.java7.** { *; }
@@ -135,6 +141,9 @@
public *;
}
+# WhisperSystems (libsignal and curve*)
+-keep class org.whispersystems.** { *; }
+
# OkHttp
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
diff --git a/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java b/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java
index 10a8cd095..f56138059 100644
--- a/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java
+++ b/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java
@@ -478,7 +478,7 @@ public void testSetup() {
@Test
public void testAutotrustedLevel() throws IOException, PGPException {
- Keyring.setAutoTrustLevel(mContext, TEST_USERID, MyUsers.Keys.TRUST_VERIFIED);
+ Keyring.setAutoTrustLevel(mContext, TEST_USERID, Keyring.TRUST_VERIFIED);
assertQueryValues(MyUsers.Keys.getUri(TEST_USERID, Keyring.VALUE_AUTOTRUST),
MyUsers.Keys.JID, TEST_USERID,
MyUsers.Keys.FINGERPRINT, Keyring.VALUE_AUTOTRUST);
@@ -486,7 +486,7 @@ public void testAutotrustedLevel() throws IOException, PGPException {
byte[] keydata = Base64.decode(TEST_KEYDATA, Base64.DEFAULT);
PGPPublicKeyRing originalKey = PGP.readPublicKeyring(keydata);
Keyring.setKey(mContext, TEST_USERID, keydata);
- PGPPublicKeyRing publicKey = Keyring.getPublicKey(mContext, TEST_USERID, MyUsers.Keys.TRUST_VERIFIED);
+ PGPPublicKeyRing publicKey = Keyring.getPublicKey(mContext, TEST_USERID, Keyring.TRUST_VERIFIED);
assertNotNull(publicKey);
assertArrayEquals(publicKey.getEncoded(), originalKey.getEncoded());
diff --git a/app/src/main/java/org/kontalk/MessagesController.java b/app/src/main/java/org/kontalk/MessagesController.java
index acfe781f8..7de10a425 100644
--- a/app/src/main/java/org/kontalk/MessagesController.java
+++ b/app/src/main/java/org/kontalk/MessagesController.java
@@ -63,7 +63,6 @@
import org.kontalk.provider.KontalkGroupCommands;
import org.kontalk.provider.MessagesProviderClient;
import org.kontalk.provider.MyMessages;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.UsersProvider;
import org.kontalk.service.DownloadService;
import org.kontalk.service.MediaService;
@@ -284,7 +283,7 @@ public boolean setTrustLevelAndRetryMessages(String jid, String fingerprint, int
throw new NullPointerException("fingerprint");
Keyring.setTrustLevel(mContext, jid, fingerprint, trustLevel);
- if (trustLevel >= MyUsers.Keys.TRUST_IGNORED) {
+ if (trustLevel >= Keyring.TRUST_IGNORED) {
retryMessagesTo(jid);
return true;
}
diff --git a/app/src/main/java/org/kontalk/authenticator/Authenticator.java b/app/src/main/java/org/kontalk/authenticator/Authenticator.java
index e04dd9afb..b2a903343 100644
--- a/app/src/main/java/org/kontalk/authenticator/Authenticator.java
+++ b/app/src/main/java/org/kontalk/authenticator/Authenticator.java
@@ -197,8 +197,11 @@ public static void exportDefaultPersonalKey(Context ctx, OutputStream dest, Stri
// trusted keys
Map trustedKeys = Keyring.getTrustedKeys(ctx);
+ String displayName = m.getUserData(acc, DATA_NAME);
+
PersonalKeyExporter exp = new PersonalKeyExporter();
- exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase, bridgeCert, trustedKeys, acc.name);
+ exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase,
+ bridgeCert, trustedKeys, acc.name, displayName);
}
public static byte[] getPrivateKeyExportData(Context ctx, String passphrase, String exportPassphrase)
diff --git a/app/src/main/java/org/kontalk/client/KontalkConnection.java b/app/src/main/java/org/kontalk/client/KontalkConnection.java
index 5a8ca18f5..65e926da5 100644
--- a/app/src/main/java/org/kontalk/client/KontalkConnection.java
+++ b/app/src/main/java/org/kontalk/client/KontalkConnection.java
@@ -42,12 +42,15 @@
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.StanzaListener;
+import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.sm.StreamManagementException;
import org.jivesoftware.smack.sm.predicates.ForMatchingPredicateOrAfterXStanzas;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
import org.jxmpp.stringprep.XmppStringprepException;
@@ -66,6 +69,8 @@ public class KontalkConnection extends XMPPTCPConnection {
/** Packet reply timeout. */
public static final int DEFAULT_PACKET_TIMEOUT = 15000;
+ private DiscoverInfo mDiscoverInfoCache;
+
protected EndpointServer mServer;
/** Actually a copy of the same Smack map, but since we need access to the listeners... */
@@ -246,10 +251,41 @@ protected void processStanza(Stanza packet) throws InterruptedException {
}
}
+ @Override
+ protected void shutdown() {
+ purgeCaches();
+ super.shutdown();
+ }
+
+ @Override
+ public synchronized void instantShutdown() {
+ purgeCaches();
+ super.instantShutdown();
+ }
+
+ private synchronized void purgeCaches() {
+ mDiscoverInfoCache = null;
+ }
+
public EndpointServer getServer() {
return mServer;
}
+ public synchronized DiscoverInfo getDiscoverInfo() throws XMPPException.XMPPErrorException,
+ SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ if (mDiscoverInfoCache == null) {
+ mDiscoverInfoCache = ServiceDiscoveryManager.getInstanceFor(this)
+ .discoverInfo(getXMPPServiceDomain());
+ }
+ return mDiscoverInfoCache;
+ }
+
+ public boolean supportsFeature(String feature) throws XMPPException.XMPPErrorException,
+ SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ DiscoverInfo cache = getDiscoverInfo();
+ return cache.containsFeature(feature);
+ }
+
@Override
public StanzaListener addStanzaIdAcknowledgedListener(String id, StanzaListener listener) throws StreamManagementException.StreamManagementNotEnabledException {
AckMultiListener multi = mStanzaIdAcknowledgedListeners.get(id);
diff --git a/app/src/main/java/org/kontalk/client/SmackInitializer.java b/app/src/main/java/org/kontalk/client/SmackInitializer.java
index 3ed397072..6606b4972 100644
--- a/app/src/main/java/org/kontalk/client/SmackInitializer.java
+++ b/app/src/main/java/org/kontalk/client/SmackInitializer.java
@@ -18,6 +18,7 @@
package org.kontalk.client;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -27,6 +28,9 @@
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smackx.iqregister.provider.RegistrationProvider;
import org.jivesoftware.smackx.iqversion.VersionManager;
+import org.jivesoftware.smackx.omemo.signal.SignalCachingOmemoStore;
+import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore;
+import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
import org.jivesoftware.smackx.xdata.provider.DataFormProvider;
import org.minidns.dnsserverlookup.android21.AndroidUsingLinkProperties;
@@ -42,6 +46,8 @@
public class SmackInitializer {
private static boolean sInitialized;
+ // TODO not sure what to do with this for now
+ private static SignalOmemoService sOmemoService;
public static void initialize(Context context) {
if (!sInitialized) {
@@ -69,6 +75,22 @@ public static void initialize(Context context) {
// we want to manually handle roster stuff
Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);
+ // initialize omemo engine
+ SignalOmemoService.acknowledgeLicense();
+ try {
+ SignalOmemoService.setup();
+ sOmemoService = (SignalOmemoService) SignalOmemoService.getInstance();
+ sOmemoService.setOmemoStoreBackend(
+ new SignalCachingOmemoStore(
+ new SignalFileBasedOmemoStore(new File(context.getFilesDir(), "omemo"))
+ )
+ );
+ }
+ catch (Exception e) {
+ // this shouldn't happen, so we just crash for now
+ throw new RuntimeException("OMEMO engine failure", e);
+ }
+
sInitialized = true;
}
}
diff --git a/app/src/main/java/org/kontalk/client/smack/SmackFuture.java b/app/src/main/java/org/kontalk/client/smack/SmackFuture.java
index d8756ef4c..08c7b6d96 100644
--- a/app/src/main/java/org/kontalk/client/smack/SmackFuture.java
+++ b/app/src/main/java/org/kontalk/client/smack/SmackFuture.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2017-2018 Florian Schmaus
+ * Copyright 2017-2020 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,9 @@
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
+import java.util.Collection;
import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -29,10 +31,10 @@
import javax.net.SocketFactory;
-import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.CallbackRecipient;
+import org.jivesoftware.smack.util.Consumer;
import org.jivesoftware.smack.util.ExceptionCallback;
import org.jivesoftware.smack.util.SuccessCallback;
@@ -50,6 +52,8 @@ public abstract class SmackFuture implements Future,
private ExceptionCallback exceptionCallback;
+ private Consumer> completionCallback;
+
@Override
public final synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (isDone()) {
@@ -89,8 +93,13 @@ public CallbackRecipient onError(ExceptionCallback exceptionCallback) {
return this;
}
+ public void onCompletion(Consumer> completionCallback) {
+ this.completionCallback = completionCallback;
+ maybeInvokeCallbacks();
+ }
+
private V getOrThrowExecutionException() throws ExecutionException {
- assert (result != null || exception != null || cancelled);
+ assert result != null || exception != null || cancelled;
if (result != null) {
return result;
}
@@ -98,7 +107,7 @@ private V getOrThrowExecutionException() throws ExecutionException {
throw new ExecutionException(exception);
}
- assert (cancelled);
+ assert cancelled;
throw new CancellationException();
}
@@ -150,11 +159,19 @@ public final synchronized V get(long timeout, TimeUnit unit)
return getOrThrowExecutionException();
}
+ public V getIfAvailable() {
+ return result;
+ }
+
protected final synchronized void maybeInvokeCallbacks() {
if (cancelled) {
return;
}
+ if ((result != null || exception != null) && completionCallback != null) {
+ completionCallback.accept(this);
+ }
+
if (result != null && successCallback != null) {
AbstractXMPPConnectionWrapper.asyncGo(new Runnable() {
@Override
@@ -294,7 +311,7 @@ public final synchronized void processStanza(Stanza stanza) {
* A simple version of InternalSmackFuture which implements isNonFatalException(E) as always returning
* false
method.
*
- * @param
+ * @param the return value of the future.
*/
public abstract static class SimpleInternalProcessStanzaSmackFuture
extends InternalProcessStanzaSmackFuture {
@@ -310,4 +327,12 @@ public static SmackFuture from(V result) {
return future;
}
+ public static boolean await(Collection extends SmackFuture, ?>> futures, long timeout, TimeUnit unit) throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(futures.size());
+ for (SmackFuture, ?> future : futures) {
+ future.onCompletion(f -> latch.countDown());
+ }
+
+ return latch.await(timeout, unit);
+ }
}
diff --git a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java
index 464374586..bbe0fb5b6 100644
--- a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java
+++ b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java
@@ -17,26 +17,19 @@
package org.kontalk.client.smack;
import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
-import java.lang.reflect.Constructor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.KeyManagementException;
-import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
-import java.security.Provider;
-import java.security.SecureRandom;
-import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
@@ -59,32 +52,24 @@
import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.PasswordCallback;
import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionConfiguration;
-import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.AlreadyConnectedException;
import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
import org.jivesoftware.smack.SmackException.ConnectionException;
+import org.jivesoftware.smack.SmackException.EndpointConnectionException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException;
-import org.jivesoftware.smack.SmackException.SmackWrappedException;
+import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.SynchronizationPoint;
import org.jivesoftware.smack.XMPPConnection;
@@ -94,6 +79,7 @@
import org.jivesoftware.smack.compress.packet.Compress;
import org.jivesoftware.smack.compress.packet.Compressed;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.datatypes.UInt16;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.IQ;
@@ -103,12 +89,8 @@
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.StreamError;
-import org.jivesoftware.smack.packet.StreamOpen;
import org.jivesoftware.smack.proxy.ProxyInfo;
-import org.jivesoftware.smack.sasl.packet.SaslStreamElements;
-import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge;
-import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
-import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
+import org.jivesoftware.smack.sasl.packet.SaslNonza;
import org.jivesoftware.smack.sm.SMUtils;
import org.jivesoftware.smack.sm.StreamManagementException;
import org.jivesoftware.smack.sm.StreamManagementException.StreamIdDoesNotMatchException;
@@ -126,24 +108,24 @@
import org.jivesoftware.smack.sm.predicates.Predicate;
import org.jivesoftware.smack.sm.provider.ParseStreamManagement;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints;
+import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint;
import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown;
import org.jivesoftware.smack.util.Async;
-import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.CloseableUtil;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.TLSUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
-import org.jivesoftware.smack.util.dns.HostAddress;
-import org.jivesoftware.smack.util.dns.SmackDaneProvider;
-import org.jivesoftware.smack.util.dns.SmackDaneVerifier;
+import org.jivesoftware.smack.util.rce.RemoteConnectionException;
+import org.jivesoftware.smack.xml.SmackXmlParser;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;
-import org.jxmpp.util.XmppStringUtils;
import org.minidns.dnsname.DnsName;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
/**
* Creates a socket connection to an XMPP server. This is the default connection
@@ -181,9 +163,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
*/
protected final PacketReader packetReader = new PacketReader();
- private final SynchronizationPoint initialOpenStreamSend = new SynchronizationPoint<>(
- this, "initial open stream element send to server");
-
/**
*
*/
@@ -196,16 +175,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
private final SynchronizationPoint compressSyncPoint = new SynchronizationPoint<>(
this, "stream compression");
- /**
- * A synchronization point which is successful if this connection has received the closing
- * stream element from the remote end-point, i.e. the server.
- */
- private final SynchronizationPoint closingStreamReceived = new SynchronizationPoint<>(
- this, "stream closing element received");
-
/**
* The default bundle and defer callback, used for new connections.
- * @see #bundleAndDeferCallback
+ * @see bundleAndDeferCallback
*/
private static BundleAndDeferCallback defaultBundleAndDeferCallback;
@@ -346,6 +318,10 @@ public void connectionClosedOnError(Exception e) {
}
}
});
+
+ // Re-init the reader and writer in case of SASL . This is done to reset the parser since a new stream
+ // is initiated.
+ buildNonzaCallback().listenFor(SaslNonza.Success.class, s -> resetParser()).install();
}
/**
@@ -358,10 +334,10 @@ public void connectionClosedOnError(Exception e) {
*
* @param jid the bare JID used by the client.
* @param password the password or authentication token.
- * @throws XmppStringprepException
+ * @throws XmppStringprepException if the provided string is invalid.
*/
public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringprepException {
- this(XmppStringUtils.parseLocalpart(jid.toString()), password, XmppStringUtils.parseDomain(jid.toString()));
+ this(XMPPTCPConnectionConfiguration.builder().setXmppAddressAndPassword(jid, password).build());
}
/**
@@ -371,10 +347,10 @@ public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringpre
* you can get fine-grained control over connection settings using the
* {@link #XMPPTCPConnection(XMPPTCPConnectionConfiguration)} constructor.
*
- * @param username
- * @param password
- * @param serviceName
- * @throws XmppStringprepException
+ * @param username TODO javadoc me please
+ * @param password TODO javadoc me please
+ * @param serviceName TODO javadoc me please
+ * @throws XmppStringprepException if the provided string is invalid.
*/
public XMPPTCPConnection(CharSequence username, String password, String serviceName) throws XmppStringprepException {
this(XMPPTCPConnectionConfiguration.builder().setUsernameAndPassword(username, password).setXmppDomain(
@@ -415,7 +391,7 @@ protected synchronized void loginInternal(String username, String password, Reso
SmackException, IOException, InterruptedException {
// Authenticate using SASL
SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null;
- saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession);
+ authenticate(username, password, config.getAuthzid(), sslSession);
// Wait for stream features after the authentication.
// TODO: The name of this synchronization point "maybeCompressFeaturesReceived" is not perfect. It should be
@@ -532,29 +508,14 @@ private void shutdown(boolean instant) {
LOGGER.finer("PacketWriter has been shut down");
if (!instant) {
- try {
- // After we send the closing stream element, check if there was already a
- // closing stream element sent by the server or wait with a timeout for a
- // closing stream element to be received from the server.
- @SuppressWarnings("unused")
- Exception res = closingStreamReceived.checkIfSuccessOrWait();
- } catch (InterruptedException | NoResponseException e) {
- LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, e);
- }
+ waitForClosingStreamTagFromServer();
}
LOGGER.finer("PacketReader shutdown()");
packetReader.shutdown();
LOGGER.finer("PacketReader has been shut down");
- final Socket socket = this.socket;
- if (socket != null && socket.isConnected()) {
- try {
- socket.close();
- } catch (Exception e) {
- LOGGER.log(Level.WARNING, "shutdown", e);
- }
- }
+ CloseableUtil.maybeClose(socket, LOGGER);
setWasAuthenticated();
@@ -595,7 +556,6 @@ protected void initState() {
compressSyncPoint.init();
smResumedSyncPoint.init();
smEnabledSyncPoint.init();
- initialOpenStreamSend.init();
}
@Override
@@ -617,20 +577,24 @@ protected void sendStanzaInternal(Stanza packet) throws NotConnectedException, I
}
private void connectUsingConfiguration() throws ConnectionException, IOException, InterruptedException {
- List failedAddresses = populateHostAddresses();
+ RemoteXmppTcpConnectionEndpoints.Result result = RemoteXmppTcpConnectionEndpoints.lookup(config);
+
+ List> connectionExceptions = new ArrayList<>();
+
SocketFactory socketFactory = config.getSocketFactory();
ProxyInfo proxyInfo = config.getProxyInfo();
int timeout = config.getConnectTimeout();
if (socketFactory == null) {
socketFactory = SocketFactory.getDefault();
}
- for (HostAddress hostAddress : hostAddresses) {
- Iterator inetAddresses;
- String host = hostAddress.getHost();
- int port = hostAddress.getPort();
+ for (Rfc6120TcpRemoteConnectionEndpoint endpoint : result.discoveredRemoteConnectionEndpoints) {
+ Iterator extends InetAddress> inetAddresses;
+ String host = endpoint.getHost().toString();
+ UInt16 portUint16 = endpoint.getPort();
+ int port = portUint16.intValue();
if (proxyInfo == null) {
- inetAddresses = hostAddress.getInetAddresses().iterator();
- assert (inetAddresses.hasNext());
+ inetAddresses = endpoint.getInetAddresses().iterator();
+ assert inetAddresses.hasNext();
innerloop: while (inetAddresses.hasNext()) {
// Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not
@@ -645,7 +609,9 @@ private void connectUsingConfiguration() throws ConnectionException, IOException
try {
socket = socketFuture.getOrThrow();
} catch (IOException e) {
- hostAddress.setException(inetAddress, e);
+ RemoteConnectionException rce = new RemoteConnectionException<>(
+ endpoint, inetAddress, e);
+ connectionExceptions.add(rce);
if (inetAddresses.hasNext()) {
continue innerloop;
} else {
@@ -655,33 +621,36 @@ private void connectUsingConfiguration() throws ConnectionException, IOException
LOGGER.finer("Established TCP connection to " + inetSocketAddress);
// We found a host to connect to, return here
this.host = host;
- this.port = port;
+ this.port = portUint16;
return;
}
- failedAddresses.add(hostAddress);
} else {
+ // TODO: Move this into the inner-loop above. There appears no reason why we should not try a proxy
+ // connection to every inet address of each connection endpoint.
socket = socketFactory.createSocket();
- StringUtils.requireNotNullOrEmpty(host, "Host of HostAddress " + hostAddress + " must not be null when using a Proxy");
+ StringUtils.requireNotNullNorEmpty(host, "Host of endpoint " + endpoint + " must not be null when using a Proxy");
final String hostAndPort = host + " at port " + port;
LOGGER.finer("Trying to establish TCP connection via Proxy to " + hostAndPort);
try {
proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout);
} catch (IOException e) {
- hostAddress.setException(e);
- failedAddresses.add(hostAddress);
+ CloseableUtil.maybeClose(socket, LOGGER);
+ RemoteConnectionException rce = new RemoteConnectionException<>(endpoint, null, e);
+ connectionExceptions.add(rce);
continue;
}
LOGGER.finer("Established TCP connection to " + hostAndPort);
// We found a host to connect to, return here
this.host = host;
- this.port = port;
+ this.port = portUint16;
return;
}
}
+
// There are no more host addresses to try
// throw an exception and report all tried
// HostAddresses in the exception
- throw ConnectionException.from(failedAddresses);
+ throw EndpointConnectionException.from(result.lookupFailures, connectionExceptions);
}
/**
@@ -690,8 +659,8 @@ private void connectUsingConfiguration() throws ConnectionException, IOException
*
* @throws XMPPException if establishing a connection to the server fails.
* @throws SmackException if the server fails to respond back or if there is anther error.
- * @throws IOException
- * @throws InterruptedException
+ * @throws IOException if an I/O error occurred.
+ * @throws InterruptedException if the calling thread was interrupted.
*/
private void initConnection() throws IOException, InterruptedException {
compressionHandler = null;
@@ -734,129 +703,23 @@ private void initReaderAndWriter() throws IOException {
* The server has indicated that TLS negotiation can start. We now need to secure the
* existing plain connection and perform a handshake. This method won't return until the
* connection has finished the handshake or an error occurred while securing the connection.
- * @throws IOException
+ * @throws IOException if an I/O error occurred.
* @throws CertificateException
- * @throws NoSuchAlgorithmException
+ * @throws NoSuchAlgorithmException if no such algorithm is available.
* @throws NoSuchProviderException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
- * @throws KeyManagementException
- * @throws SmackException
+ * @throws KeyManagementException if there was a key mangement error.
+ * @throws SmackException if Smack detected an exceptional situation.
* @throws Exception if an exception occurs.
*/
@SuppressWarnings("LiteralClassName")
private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException {
- SmackDaneVerifier daneVerifier = null;
-
- if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) {
- SmackDaneProvider daneProvider = DNSUtil.getDaneProvider();
- if (daneProvider == null) {
- throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured");
- }
- daneVerifier = daneProvider.newInstance();
- if (daneVerifier == null) {
- throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier");
- }
- }
-
- SSLContext context = this.config.getCustomSSLContext();
- KeyStore ks = null;
- PasswordCallback pcb = null;
-
- if (context == null) {
- final String keyStoreType = config.getKeystoreType();
- final CallbackHandler callbackHandler = config.getCallbackHandler();
- final String keystorePath = config.getKeystorePath();
- if ("PKCS11".equals(keyStoreType)) {
- try {
- Constructor> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
- String pkcs11Config = "name = SmartCard\nlibrary = " + config.getPKCS11Library();
- ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes(StringUtils.UTF8));
- Provider p = (Provider) c.newInstance(config);
- Security.addProvider(p);
- ks = KeyStore.getInstance("PKCS11",p);
- pcb = new PasswordCallback("PKCS11 Password: ",false);
- callbackHandler.handle(new Callback[] {pcb});
- ks.load(null,pcb.getPassword());
- }
- catch (Exception e) {
- LOGGER.log(Level.WARNING, "Exception", e);
- ks = null;
- }
- }
- else if ("Apple".equals(keyStoreType)) {
- ks = KeyStore.getInstance("KeychainStore","Apple");
- ks.load(null,null);
- // pcb = new PasswordCallback("Apple Keychain",false);
- // pcb.setPassword(null);
- }
- else if (keyStoreType != null) {
- ks = KeyStore.getInstance(keyStoreType);
- if (callbackHandler != null && StringUtils.isNotEmpty(keystorePath)) {
- try {
- pcb = new PasswordCallback("Keystore Password: ", false);
- callbackHandler.handle(new Callback[] { pcb });
- ks.load(new FileInputStream(keystorePath), pcb.getPassword());
- }
- catch (Exception e) {
- LOGGER.log(Level.WARNING, "Exception", e);
- ks = null;
- }
- } else {
- ks.load(null, null);
- }
- }
-
- KeyManager[] kms = null;
-
- if (ks != null) {
- String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
- KeyManagerFactory kmf = null;
- try {
- kmf = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm);
- }
- catch (NoSuchAlgorithmException e) {
- LOGGER.log(Level.FINE, "Could get the default KeyManagerFactory for the '"
- + keyManagerFactoryAlgorithm + "' algorithm", e);
- }
- if (kmf != null) {
- try {
- if (pcb == null) {
- kmf.init(ks, null);
- }
- else {
- kmf.init(ks, pcb.getPassword());
- pcb.clearPassword();
- }
- kms = kmf.getKeyManagers();
- }
- catch (NullPointerException npe) {
- LOGGER.log(Level.WARNING, "NullPointerException", npe);
- }
- }
- }
-
- // If the user didn't specify a SSLContext, use the default one
- context = SSLContext.getInstance("TLS");
-
- final SecureRandom secureRandom = new java.security.SecureRandom();
- X509TrustManager customTrustManager = config.getCustomX509TrustManager();
-
- if (daneVerifier != null) {
- // User requested DANE verification.
- daneVerifier.init(context, kms, customTrustManager, secureRandom);
- } else {
- TrustManager[] customTrustManagers = null;
- if (customTrustManager != null) {
- customTrustManagers = new TrustManager[] { customTrustManager };
- }
- context.init(kms, customTrustManagers, secureRandom);
- }
- }
+ SmackTlsContext smackTlsContext = getSmackTlsContext();
Socket plain = socket;
// Secure the plain connection
- socket = context.getSocketFactory().createSocket(plain,
+ socket = smackTlsContext.sslContext.getSocketFactory().createSocket(plain,
config.getXMPPServiceDomain().toString(), plain.getPort(), true);
final SSLSocket sslSocket = (SSLSocket) socket;
@@ -871,8 +734,8 @@ else if (keyStoreType != null) {
// Proceed to do the handshake
sslSocket.startHandshake();
- if (daneVerifier != null) {
- daneVerifier.finish(sslSocket);
+ if (smackTlsContext.daneVerifier != null) {
+ smackTlsContext.daneVerifier.finish(sslSocket.getSession());
}
final HostnameVerifier verifier = getConfiguration().getHostnameVerifier();
@@ -942,10 +805,10 @@ public boolean isUsingCompression() {
* before authentication took place.
*
*
- * @throws NotConnectedException
- * @throws SmackException
- * @throws NoResponseException
- * @throws InterruptedException
+ * @throws NotConnectedException if the XMPP connection is not connected.
+ * @throws SmackException if Smack detected an exceptional situation.
+ * @throws NoResponseException if there was no response from the remote entity.
+ * @throws InterruptedException if the calling thread was interrupted.
*/
private void maybeEnableCompression() throws SmackException, InterruptedException {
if (!config.isCompressionEnabled()) {
@@ -975,13 +838,12 @@ private void maybeEnableCompression() throws SmackException, InterruptedExceptio
*
*
* @throws XMPPException if an error occurs while trying to establish the connection.
- * @throws SmackException
- * @throws IOException
- * @throws InterruptedException
+ * @throws SmackException if Smack detected an exceptional situation.
+ * @throws IOException if an I/O error occurred.
+ * @throws InterruptedException if the calling thread was interrupted.
*/
@Override
protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException {
- closingStreamReceived.init();
// Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if
// there is an error establishing the connection
connectUsingConfiguration();
@@ -996,54 +858,10 @@ protected void connectInternal() throws SmackException, IOException, XMPPExcepti
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
}
- /**
- * Sends out a notification that there was an error with the connection
- * and closes the connection. Also prints the stack trace of the given exception
- *
- * @param e the exception that causes the connection close event.
- */
- private void notifyConnectionError(final Exception e) {
- ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() {
- @Override
- public void run() {
- // Listeners were already notified of the exception, return right here.
- if (packetReader.done || packetWriter.done()) return;
-
- // Report the failure outside the synchronized block, so that a thread waiting within a synchronized
- // function like connect() throws the wrapped exception.
- SmackWrappedException smackWrappedException = new SmackWrappedException(e);
- tlsHandled.reportGenericFailure(smackWrappedException);
- saslFeatureReceived.reportGenericFailure(smackWrappedException);
- maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException);
- lastFeaturesReceived.reportGenericFailure(smackWrappedException);
-
- synchronized (XMPPTCPConnection.this) {
- // Within this synchronized block, either *both* reader and writer threads must be terminated, or
- // none.
- assert ((packetReader.done && packetWriter.done())
- || (!packetReader.done && !packetWriter.done()));
-
- // Closes the connection temporary. A reconnection is possible
- // Note that a connection listener of XMPPTCPConnection will drop the SM state in
- // case the Exception is a StreamErrorException.
- instantShutdown();
- }
-
- Async.go(new Runnable() {
- @Override
- public void run() {
- // Notify connection listeners of the error.
- callConnectionClosedOnErrorListener(e);
- }
- }, XMPPTCPConnection.this + " callConnectionClosedOnErrorListener()");
- }
- });
- }
-
/**
* For unit testing purposes
*
- * @param writer
+ * @param writer TODO javadoc me please
*/
protected void setWriter(Writer writer) {
this.writer = writer;
@@ -1068,7 +886,7 @@ protected void afterFeaturesReceived() throws NotConnectedException, Interrupted
tlsHandled.reportSuccess();
}
- if (getSASLAuthentication().authenticationSuccessful()) {
+ if (isSaslAuthenticated()) {
// If we have received features after the SASL has been successfully completed, then we
// have also *maybe* received, as it is an optional feature, the compression feature
// from the server.
@@ -1076,33 +894,17 @@ protected void afterFeaturesReceived() throws NotConnectedException, Interrupted
}
}
- /**
- * Resets the parser using the latest connection's reader. Resetting the parser is necessary
- * when the plain connection has been secured or when a new opening stream element is going
- * to be sent by the server.
- *
- * @throws SmackException if the parser could not be reset.
- * @throws InterruptedException
- */
- void openStream() throws SmackException, InterruptedException {
- // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as
- // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external
- // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first
- // response from the server (see e.g. RFC 6120 ยง 9.1.1 Step 2.)
- CharSequence to = getXMPPServiceDomain();
- CharSequence from = null;
- CharSequence localpart = config.getUsername();
- if (localpart != null) {
- from = XmppStringUtils.completeJidFrom(localpart, to);
- }
- String id = getStreamId();
- sendNonza(new StreamOpen(to, from, id));
+ private void resetParser() throws IOException {
try {
- packetReader.parser = PacketParserUtils.newXmppParser(reader);
+ packetReader.parser = SmackXmlParser.newXmlParser(reader);
+ } catch (XmlPullParserException e) {
+ throw new IOException(e);
}
- catch (XmlPullParserException e) {
- throw new SmackException(e);
}
+
+ private void openStreamAndResetParser() throws IOException, NotConnectedException, InterruptedException {
+ sendStreamOpen();
+ resetParser();
}
protected class PacketReader {
@@ -1145,12 +947,14 @@ void shutdown() {
* Parse top-level packets in order to process them further.
*/
private void parsePackets() {
+ boolean initialStreamOpenSend = false;
try {
- initialOpenStreamSend.checkIfSuccessOrWait();
- int eventType = parser.getEventType();
+ openStreamAndResetParser();
+ initialStreamOpenSend = true;
+ XmlPullParser.Event eventType = parser.getEventType();
while (!done) {
switch (eventType) {
- case XmlPullParser.START_TAG:
+ case START_ELEMENT:
final String name = parser.getName();
switch (name) {
case Message.ELEMENT:
@@ -1163,12 +967,7 @@ private void parsePackets() {
}
break;
case "stream":
- // We found an opening stream.
- if ("jabber:client".equals(parser.getNamespace(null))) {
- streamId = parser.getAttributeValue("", "id");
- String reportedServerDomain = parser.getAttributeValue("", "from");
- assert (config.getXMPPServiceDomain().equals(reportedServerDomain));
- }
+ onStreamOpen(parser);
break;
case "error":
StreamError streamError = PacketParserUtils.parseStreamError(parser);
@@ -1179,17 +978,17 @@ private void parsePackets() {
tlsHandled.reportSuccess();
throw new StreamErrorException(streamError);
case "features":
- parseFeatures(parser);
+ parseFeaturesAndNotify(parser);
break;
case "proceed":
try {
// Secure the connection by negotiating TLS
proceedTLSReceived();
// Send a new opening stream to the server
- openStream();
+ openStreamAndResetParser();
}
catch (Exception e) {
- SmackException smackException = new SmackException(e);
+ SmackException.SmackWrappedException smackException = new SmackException.SmackWrappedException(e);
tlsHandled.reportFailure(smackException);
throw e;
}
@@ -1200,44 +999,26 @@ private void parsePackets() {
case "urn:ietf:params:xml:ns:xmpp-tls":
// TLS negotiation has failed. The server will close the connection
// TODO Parse failure stanza
- throw new SmackException("TLS negotiation has failed");
+ throw new SmackException.SmackMessageException("TLS negotiation has failed");
case "http://jabber.org/protocol/compress":
// Stream compression has been denied. This is a recoverable
// situation. It is still possible to authenticate and
// use the connection but using an uncompressed connection
// TODO Parse failure stanza
- compressSyncPoint.reportFailure(new SmackException(
+ compressSyncPoint.reportFailure(new SmackException.SmackMessageException(
"Could not establish compression"));
break;
- case SaslStreamElements.NAMESPACE:
- // SASL authentication has failed. The server may close the connection
- // depending on the number of retries
- final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
- getSASLAuthentication().authenticationFailed(failure);
- break;
+ default:
+ parseAndProcessNonza(parser);
}
break;
- case Challenge.ELEMENT:
- // The server is challenging the SASL authentication made by the client
- String challengeData = parser.nextText();
- getSASLAuthentication().challengeReceived(challengeData);
- break;
- case Success.ELEMENT:
- Success success = new Success(parser.nextText());
- // We now need to bind a resource for the connection
- // Open a new stream and wait for the response
- openStream();
- // The SASL authentication with the server was successful. The next step
- // will be to bind the resource
- getSASLAuthentication().authenticated(success);
- break;
case Compressed.ELEMENT:
// Server confirmed that it's possible to use stream compression. Start
// stream compression
// Initialize the reader and writer with the new compressed version
initReaderAndWriter();
// Send a new opening stream to the server
- openStream();
+ openStreamAndResetParser();
// Notify that compression is being used
compressSyncPoint.reportSuccess();
break;
@@ -1246,7 +1027,7 @@ private void parsePackets() {
if (enabled.isResumeSet()) {
smSessionId = enabled.getId();
if (StringUtils.isNullOrEmpty(smSessionId)) {
- SmackException xmppException = new SmackException("Stream Management 'enabled' element with resume attribute but without session id received");
+ SmackException xmppException = new SmackException.SmackMessageException("Stream Management 'enabled' element with resume attribute but without session id received");
smEnabledSyncPoint.reportFailure(xmppException);
throw xmppException;
}
@@ -1276,7 +1057,7 @@ private void parsePackets() {
if (!smEnabledSyncPoint.requestSent()) {
throw new IllegalStateException("Failed element received but SM was not previously enabled");
}
- smEnabledSyncPoint.reportFailure(new SmackException(xmppException));
+ smEnabledSyncPoint.reportFailure(new SmackException.SmackWrappedException(xmppException));
// Report success for last lastFeaturesReceived so that in case a
// failed resumption, we can continue with normal resource binding.
// See text of XEP-198 5. below Example 11.
@@ -1328,12 +1109,12 @@ private void parsePackets() {
LOGGER.warning("SM Ack Request received while SM is not enabled");
}
break;
- default:
- LOGGER.warning("Unknown top level stream element: " + name);
- break;
+ default:
+ parseAndProcessNonza(parser);
+ break;
}
break;
- case XmlPullParser.END_TAG:
+ case END_ELEMENT:
final String endTagName = parser.getName();
if ("stream".equals(endTagName)) {
if (!parser.getNamespace().equals("http://etherx.jabber.org/streams")) {
@@ -1369,20 +1150,25 @@ public void run() {
}
}
break;
- case XmlPullParser.END_DOCUMENT:
+ case END_DOCUMENT:
// END_DOCUMENT only happens in an error case, as otherwise we would see a
// closing stream element before.
- throw new SmackException(
+ throw new SmackException.SmackMessageException(
"Parser got END_DOCUMENT event. This could happen e.g. if the server closed the connection without sending a closing stream element");
+ default:
+ // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
+ break;
}
eventType = parser.next();
}
}
catch (Exception e) {
+ // TODO: Move the call closingStreamReceived.reportFailure(e) into notifyConnectionError?
closingStreamReceived.reportFailure(e);
// The exception can be ignored if the the connection is 'done'
- // or if the it was caused because the socket got closed
- if (!(done || packetWriter.queue.isShutdown())) {
+ // or if the it was caused because the socket got closed. It can not be ignored if it
+ // happened before (or while) the initial stream opened was send.
+ if (!(done || packetWriter.queue.isShutdown()) || !initialStreamOpenSend) {
// Close the connection and notify connection listeners of the
// error.
notifyConnectionError(e);
@@ -1393,6 +1179,8 @@ public void run() {
protected class PacketWriter {
public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE;
+ public static final int UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE = 1024;
+ public static final int UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE_HIGH_WATER_MARK = (int) (0.3 * UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE);
private final String threadName = "Smack Writer (" + getConnectionCounter() + ')';
@@ -1416,7 +1204,7 @@ protected class PacketWriter {
* True if some preconditions are given to start the bundle and defer mechanism.
*
* This will likely get set to true right after the start of the writer thread, because
- * {@link #nextStreamElement()} will check if {@link #queue} is empty, which is probably the case, and then set
+ * {@link #nextStreamElement()} will check if {@link queue} is empty, which is probably the case, and then set
* this field to true.
*
*/
@@ -1472,8 +1260,8 @@ protected void throwNotConnectedExceptionIfDoneAndResumptionNotPossible() throws
* Sends the specified element to the server.
*
* @param element the element to send.
- * @throws NotConnectedException
- * @throws InterruptedException
+ * @throws NotConnectedException if the XMPP connection is not connected.
+ * @throws InterruptedException if the calling thread was interrupted.
*/
protected void sendStreamElement(Element element) throws NotConnectedException, InterruptedException {
throwNotConnectedExceptionIfDoneAndResumptionNotPossible();
@@ -1494,7 +1282,7 @@ protected void sendStreamElement(Element element) throws NotConnectedException,
/**
* Shuts down the stanza writer. Once this method has been called, no further
* packets will be written to the server.
- * @throws InterruptedException
+ * @throws InterruptedException if the calling thread was interrupted.
*/
void shutdown(boolean instant) {
instantShutdown = instant;
@@ -1537,8 +1325,6 @@ private Element nextStreamElement() {
private void writePackets() {
Exception writerException = null;
try {
- openStream();
- initialOpenStreamSend.reportSuccess();
// Write out packets from the queue.
while (!done()) {
Element element = nextStreamElement();
@@ -1580,13 +1366,13 @@ else if (element instanceof Enable) {
// The client needs to add messages to the unacknowledged stanzas queue
// right after it sent 'enabled'. Stanza will be added once
// unacknowledgedStanzas is not null.
- unacknowledgedStanzas = new ArrayBlockingQueue<>(QUEUE_SIZE);
+ unacknowledgedStanzas = new ArrayBlockingQueue<>(UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE);
}
maybeAddToUnacknowledgedStanzas(packet);
- CharSequence elementXml = element.toXML(StreamOpen.CLIENT_NAMESPACE);
+ CharSequence elementXml = element.toXML(outgoingStreamXmlEnvironment);
if (elementXml instanceof XmlStringBuilder) {
- ((XmlStringBuilder) elementXml).write(writer, StreamOpen.CLIENT_NAMESPACE);
+ ((XmlStringBuilder) elementXml).write(writer, outgoingStreamXmlEnvironment);
}
else {
writer.write(elementXml.toString());
@@ -1608,7 +1394,7 @@ else if (element instanceof Enable) {
Stanza stanza = (Stanza) packet;
maybeAddToUnacknowledgedStanzas(stanza);
}
- writer.write(packet.toXML(null).toString());
+ writer.write(packet.toXML().toString());
}
writer.flush();
}
@@ -1682,12 +1468,12 @@ private void maybeAddToUnacknowledgedStanzas(Stanza stanza) throws IOException {
// packet order is not stable at this point (sendStanzaInternal() can be
// called concurrently).
if (unacknowledgedStanzas != null && stanza != null) {
- // If the unacknowledgedStanza queue is nearly full, request an new ack
+ // If the unacknowledgedStanza queue reaching its high water mark, request an new ack
// from the server in order to drain it
- if (unacknowledgedStanzas.size() == 0.8 * XMPPTCPConnection.QUEUE_SIZE) {
- writer.write(AckRequest.INSTANCE.toXML(null).toString());
- writer.flush();
+ if (unacknowledgedStanzas.size() == UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE_HIGH_WATER_MARK) {
+ writer.write(AckRequest.INSTANCE.toXML().toString());
}
+
try {
// It is important the we put the stanza in the unacknowledged stanza
// queue before we put it on the wire
@@ -1807,7 +1593,7 @@ public void removeAllRequestAckPredicates() {
*
* @throws StreamManagementNotEnabledException if Stream Management is not enabled.
* @throws NotConnectedException if the connection is not connected.
- * @throws InterruptedException
+ * @throws InterruptedException if the calling thread was interrupted.
*/
public void requestSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException {
if (!isSmEnabled()) {
@@ -1830,7 +1616,7 @@ private void requestSmAcknowledgementInternal() throws NotConnectedException, In
*
* @throws StreamManagementNotEnabledException if Stream Management is not enabled.
* @throws NotConnectedException if the connection is not connected.
- * @throws InterruptedException
+ * @throws InterruptedException if the calling thread was interrupted.
*/
public void sendSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException {
if (!isSmEnabled()) {
@@ -2107,7 +1893,7 @@ public void run() {
/**
* Set the default bundle and defer callback used for new connections.
*
- * @param defaultBundleAndDeferCallback
+ * @param defaultBundleAndDeferCallback TODO javadoc me please
* @see BundleAndDeferCallback
* @since 4.1
*/
diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java
index 637cb09ad..7f38974ac 100644
--- a/app/src/main/java/org/kontalk/crypto/Coder.java
+++ b/app/src/main/java/org/kontalk/crypto/Coder.java
@@ -25,6 +25,8 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
+import org.jivesoftware.smack.packet.Message;
+
/**
* Generic coder interface.
@@ -40,6 +42,7 @@ public abstract class Coder {
/** Cleartext messages. Not encrypted nor signed. */
public static final int SECURITY_CLEARTEXT = 0;
/** Legacy (2.x) encryption method. For compatibility with old messages. */
+ @Deprecated
public static final int SECURITY_LEGACY_ENCRYPTED = 1;
/** Basic encryption (e.g. PGP encrypted). Safe enough. */
public static final int SECURITY_BASIC_ENCRYPTED = 1 << 1;
@@ -78,17 +81,20 @@ public abstract class Coder {
/** Basic encryption (e.g. PGP). */
public static final int SECURITY_BASIC = SECURITY_BASIC_ENCRYPTED | SECURITY_BASIC_SIGNED;
+ /** Advanced encryption (e.g. OTR). */
+ public static final int SECURITY_ADVANCED = SECURITY_ADVANCED_ENCRYPTED | SECURITY_ADVANCED_SIGNED;
+
/** How much time to consider a message timestamp drifted (and thus compromised). */
public static final long TIMEDIFF_THRESHOLD = TimeUnit.DAYS.toMillis(1);
- /** Encrypts a string. */
- public abstract byte[] encryptText(CharSequence text) throws GeneralSecurityException;
+ /** Returns the supported security flags for the coder. */
+ public abstract int getSupportedFlags();
- /** Encrypts a stanza. */
- public abstract byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException;
+ /** Encrypts a message stanza. */
+ public abstract Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException;
- /** Decrypts a byte array which should contain text. */
- public abstract DecryptOutput decryptText(byte[] encrypted, boolean verify)
+ /** Decrypts a message stanza. */
+ public abstract DecryptOutput decryptMessage(Message message, boolean verify)
throws GeneralSecurityException;
/** Encrypts a file. */
@@ -113,16 +119,54 @@ public static boolean isError(int securityFlags) {
(securityFlags & SECURITY_ERROR_PUBLIC_KEY_UNAVAILABLE) != 0;
}
+ /**
+ * Calculate the common security flags supported by all given users flags.
+ * @param requestedFlags what flags we would like
+ * @param supportedFlags what flags users support
+ * @return calculated flags
+ */
+ public static int getCompatibleSecurityFlags(int requestedFlags, int[] supportedFlags) {
+ // FIXME not really checking flags, but I know what I'm doing here and also I'm lazy
+ int finalFlags = -1;
+ for (int flags : supportedFlags) {
+ if (flags < 0) {
+ // unknown security, just skip it
+ continue;
+ }
+ int calculatedFlags = getCompatibleSecurityFlags(requestedFlags, flags);
+ if (finalFlags >= 0) {
+ finalFlags = Math.min(finalFlags, calculatedFlags);
+ }
+ else {
+ finalFlags = calculatedFlags;
+ }
+ }
+ return finalFlags < 0 ? requestedFlags : finalFlags;
+ }
+
+ /**
+ * Calculate the security flags supported by the given users flags.
+ * @param requestedFlags what flags we would like
+ * @param supportedFlags what flags the user supports
+ * @return calculated flags
+ */
+ public static int getCompatibleSecurityFlags(int requestedFlags, int supportedFlags) {
+ // FIXME not really checking flags, but I know what I'm doing here and also I'm lazy
+ return Math.min(requestedFlags, supportedFlags);
+ }
+
public static class DecryptOutput {
public final String mime;
- public final String cleartext;
+ public final Message cleartext;
public final Date timestamp;
+ public final int securityFlags;
public final List errors;
- DecryptOutput(String cleartext, String mime, Date timestamp, List errors) {
+ DecryptOutput(Message cleartext, String mime, Date timestamp, int securityFlags, List errors) {
this.cleartext = cleartext;
this.mime = mime;
this.timestamp = timestamp;
+ this.securityFlags = securityFlags;
this.errors = errors;
}
}
diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java
new file mode 100644
index 000000000..4dd16953a
--- /dev/null
+++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java
@@ -0,0 +1,240 @@
+/*
+ * Kontalk Android client
+ * Copyright (C) 2018 Kontalk Devteam
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.kontalk.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.MessageBuilder;
+import org.jivesoftware.smackx.omemo.OmemoMessage;
+import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
+import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
+import org.jivesoftware.smackx.omemo.OmemoManager;
+import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
+import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
+import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
+import org.jivesoftware.smackx.pubsub.PubSubException;
+import org.jivesoftware.smackx.pubsub.packet.PubSub;
+import org.jxmpp.jid.BareJid;
+import org.jxmpp.jid.Jid;
+
+import org.kontalk.client.KontalkConnection;
+import org.kontalk.provider.Keyring;
+
+
+/**
+ * OMEMO coder implementation.
+ * @author Daniele Ricci
+ */
+public class OmemoCoder extends Coder {
+
+ private final TrustedRecipient[] mRecipients;
+ private final BareJid mSender;
+
+ private OmemoManager mManager;
+
+ /** For encryption. */
+ public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients)
+ throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
+ InterruptedException, SmackException.NoResponseException,
+ SmackException.NotLoggedInException, CorruptedOmemoKeyException,
+ IOException, CannotEstablishOmemoSessionException,
+ PubSubException.NotALeafNodeException {
+
+ init(connection);
+
+ mSender = null;
+ mRecipients = recipients;
+
+ if (recipients != null) {
+ for (TrustedRecipient rcpt : recipients) {
+ BareJid user = rcpt.jid.asBareJid();
+ Map fingerprints = mManager
+ .getActiveFingerprints(user);
+
+ if (fingerprints.isEmpty()) {
+ if (!mManager.contactSupportsOmemo(user)) {
+ throw new UnsupportedOperationException("Recipient " + user + " does not support OMEMO");
+ }
+
+ // fingerprints should be available after contactSupportsOmemo()
+ fingerprints = mManager.getActiveFingerprints(user);
+ }
+
+ // Trust the OMEMO fingerprints by looking at user trust information.
+ // Unknown trust level means a new key came in recently and was not ignored nor verified.
+ // When that meets manual trust, it means user exited from Blind Trust Before Verification.
+ // In that case, identities will not be trusted and encryption will fail.
+ boolean willTrust = !(rcpt.trustLevel == Keyring.TRUST_UNKNOWN && rcpt.manualTrust);
+ for (Map.Entry device : fingerprints.entrySet()) {
+ if (willTrust) {
+ mManager.trustOmemoIdentity(device.getKey(), device.getValue());
+ }
+ else {
+ mManager.distrustOmemoIdentity(device.getKey(), device.getValue());
+ }
+ }
+ }
+ }
+ }
+
+ /** For decryption. */
+ public OmemoCoder(XMPPConnection connection, Jid sender) throws XMPPException.XMPPErrorException,
+ SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ init(connection);
+
+ mSender = sender.asBareJid();
+ mRecipients = null;
+ }
+
+ private void init(XMPPConnection connection) throws XMPPException.XMPPErrorException,
+ SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ // FIXME should be: if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) {
+ if (!((KontalkConnection) connection).supportsFeature(PubSub.NAMESPACE)) {
+ throw new UnsupportedOperationException("Server does not support OMEMO");
+ }
+
+ mManager = OmemoManager.getInstanceFor(connection);
+ }
+
+ @Override
+ public int getSupportedFlags() {
+ return Coder.SECURITY_ADVANCED;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException {
+ Message output;
+ Set recipients = new HashSet<>(mRecipients.length);
+ for (TrustedRecipient rcpt : mRecipients) {
+ recipients.add(rcpt.jid.asBareJid());
+ }
+
+ OmemoMessage.Sent sentMessage;
+ try {
+ sentMessage = mManager.encrypt(recipients, message.getBody());
+ }
+ catch (UndecidedOmemoIdentityException e) {
+ // TODO experimenting; crash for now
+ throw new RuntimeException("Impossible: we should have decided already!", e);
+ }
+ catch (Exception e) {
+ throw new GeneralSecurityException(e);
+ }
+
+ if (sentMessage.isMissingRecipients()) {
+ // some devices were skipped for some reason
+ // TODO experimenting; crash for now
+ throw new RuntimeException("some recipients were missed",
+ sentMessage.getSkippedDevices().values().iterator().next());
+ }
+ else {
+ return sentMessage.buildMessage(MessageBuilder
+ .buildMessage(message.getStanzaId())
+ .ofType(message.getType())
+ .addExtensions(message.getExtensions()), message.getTo());
+ }
+ }
+
+ /**
+ * For now just here to fool {@link org.kontalk.service.msgcenter.MessageListener}.
+ */
+ @Override
+ public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException {
+ // TODO we need to understand how this is working
+ /*
+ if (message.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE)) {
+ // offline message - decrypt manually
+ ClearTextMessage cleartext;
+ try {
+ cleartext = mManager.decrypt(mSender, message.getExtension(OmemoElement.NAME_ENCRYPTED, ...));
+ }
+ catch (Exception e) {
+ throw new GeneralSecurityException("OMEMO decryption failed", e);
+ }
+
+ if (cleartext.getBody() == null) {
+ throw new DecryptException(DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND);
+ }
+
+ // simple text message
+ Message output = new Message();
+ output.setStanzaId(message.getStanzaId());
+ output.setType(message.getType());
+ output.setFrom(message.getFrom());
+ output.setTo(message.getTo());
+ output.setBody(cleartext.getBody());
+ // copy extensions and remove our own
+ output.addExtensions(message.getExtensions());
+ output.removeExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL);
+ return new DecryptOutput(output, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList());
+ }
+ else {
+ */
+ // online message - already decrypted by smack-omemo
+ return new DecryptOutput(message, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList());
+ //}
+ }
+
+ @Override
+ public void encryptFile(InputStream input, OutputStream output) throws GeneralSecurityException {
+ throw new UnsupportedOperationException("OMEMO does not support file encryption");
+ }
+
+ @Override
+ public void decryptFile(InputStream input, boolean verify, OutputStream output, List errors) throws GeneralSecurityException {
+ throw new UnsupportedOperationException("OMEMO does not support file encryption");
+ }
+
+ @Override
+ public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecurityException {
+ throw new UnsupportedOperationException("OMEMO does not support verification and signing");
+ }
+
+ /**
+ * Recipient information for encryption.
+ * The trust level is considered blocking if TRUST_UKNOWN and manualTrust is true,
+ * meaning we manually verified a previous key and thus overridden Blind Trust
+ * Before Verification.
+ */
+ public static class TrustedRecipient {
+ public final Jid jid;
+ public final int trustLevel;
+ public final boolean manualTrust;
+
+ public TrustedRecipient(Jid jid, int trustLevel, boolean manualTrust) {
+ this.jid = jid;
+ this.trustLevel = trustLevel;
+ this.manualTrust = manualTrust;
+ }
+ }
+}
diff --git a/app/src/main/java/org/kontalk/crypto/PGPCoder.java b/app/src/main/java/org/kontalk/crypto/PGPCoder.java
index 5127115fd..1c0e49792 100644
--- a/app/src/main/java/org/kontalk/crypto/PGPCoder.java
+++ b/app/src/main/java/org/kontalk/crypto/PGPCoder.java
@@ -32,6 +32,8 @@
import java.util.Iterator;
import java.util.List;
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.Message;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
@@ -58,6 +60,7 @@
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator;
+import org.kontalk.client.E2EEncryption;
import org.kontalk.client.EndpointServer;
import org.kontalk.message.TextComponent;
import org.kontalk.util.CPIMMessage;
@@ -71,7 +74,6 @@
import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_INVALID_TIMESTAMP;
import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND;
import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_VERIFICATION_FAILED;
-
import static org.kontalk.crypto.VerifyException.VERIFY_EXCEPTION_INVALID_DATA;
import static org.kontalk.crypto.VerifyException.VERIFY_EXCEPTION_VERIFICATION_FAILED;
@@ -111,6 +113,11 @@ public PGPCoder(EndpointServer server, PersonalKey key, PGPPublicKeyRing sender)
}
@Override
+ public int getSupportedFlags() {
+ return Coder.SECURITY_BASIC;
+ }
+
+ @Deprecated
public byte[] encryptText(CharSequence text) throws GeneralSecurityException {
try {
// consider plain text
@@ -126,8 +133,7 @@ public byte[] encryptText(CharSequence text) throws GeneralSecurityException {
}
}
- @Override
- public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException {
+ private byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException {
try {
// prepare XML wrapper
final String xmlWrapper =
@@ -147,6 +153,19 @@ public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException {
}
}
+ @Override
+ public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException {
+ byte[] toMessage = encryptStanza(message.toXML());
+
+ org.jivesoftware.smack.packet.Message encMsg =
+ new org.jivesoftware.smack.packet.Message(message.getTo(), message.getType());
+
+ encMsg.setBody(placeholder);
+ encMsg.setStanzaId(message.getStanzaId());
+ encMsg.addExtension(new E2EEncryption(toMessage));
+ return encMsg;
+ }
+
private byte[] encryptData(String mime, CharSequence data)
throws PGPException, IOException, SignatureException {
@@ -220,9 +239,21 @@ private byte[] encryptData(String mime, CharSequence data)
return out.toByteArray();
}
- @SuppressWarnings("unchecked")
@Override
- public DecryptOutput decryptText(byte[] encrypted, boolean verify)
+ public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException {
+ ExtensionElement _encrypted = message.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE);
+ if (!(_encrypted instanceof E2EEncryption)) {
+ throw new DecryptException(DECRYPT_EXCEPTION_INVALID_DATA, "Not an encrypted message");
+ }
+
+ E2EEncryption encrypted = (E2EEncryption) _encrypted;
+ byte[] encryptedData = encrypted.getData();
+
+ return decryptText(encryptedData, message, verify);
+ }
+
+ @SuppressWarnings("unchecked")
+ private DecryptOutput decryptText(byte[] encrypted, Message origin, boolean verify)
throws GeneralSecurityException {
List errors = new ArrayList<>();
@@ -498,7 +529,35 @@ public DecryptOutput decryptText(byte[] encrypted, boolean verify)
DataUtils.close(cDataIn);
}
- return new DecryptOutput(out, mime, timestamp, errors);
+ Message message;
+ if (XMPPUtils.XML_XMPP_TYPE.equalsIgnoreCase(mime)) {
+ try {
+ message = XMPPUtils.parseMessageStanza(out);
+ }
+ catch (Exception e) {
+ throw new DecryptException(DECRYPT_EXCEPTION_INVALID_DATA, e);
+ }
+
+ if (timestamp != null && !checkDriftedDelay(message, timestamp)) {
+ errors.add(new DecryptException(DECRYPT_EXCEPTION_INVALID_TIMESTAMP,
+ "Drifted timestamp"));
+ }
+
+ // extensions won't be copied because the new message will take over
+ }
+ else {
+ // simple text message
+ message = new Message();
+ message.setType(origin.getType());
+ message.setFrom(origin.getFrom());
+ message.setTo(origin.getTo());
+ message.setBody(out);
+ // copy extensions and remove our own
+ message.addExtensions(message.getExtensions());
+ message.removeExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE);
+ }
+
+ return new DecryptOutput(message, mime, timestamp, SECURITY_BASIC, errors);
}
@Override
@@ -848,4 +907,17 @@ public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecu
return new VerifyOutput(out, timestamp, errors);
}
+ private static boolean checkDriftedDelay(Message m, Date expected) {
+ Date stamp = XMPPUtils.getStanzaDelay(m);
+ if (stamp != null) {
+ long time = stamp.getTime();
+ long now = expected.getTime();
+ long diff = Math.abs(now - time);
+ return (diff < Coder.TIMEDIFF_THRESHOLD);
+ }
+
+ // no timestamp found
+ return true;
+ }
+
}
diff --git a/app/src/main/java/org/kontalk/crypto/PGPUserID.java b/app/src/main/java/org/kontalk/crypto/PGPUserID.java
index 0ff09a086..e36f68d85 100644
--- a/app/src/main/java/org/kontalk/crypto/PGPUserID.java
+++ b/app/src/main/java/org/kontalk/crypto/PGPUserID.java
@@ -29,6 +29,7 @@
public class PGPUserID {
private static final Pattern PATTERN_UID_FULL = Pattern.compile("^(.*) \\((.*)\\) <(.*)>$");
private static final Pattern PATTERN_UID_NO_COMMENT = Pattern.compile("^(.*) <(.*)>$");
+ private static final Pattern PATTERN_UID_EMAIL_ONLY = Pattern.compile("^(.*@.*)$");
private final String name;
private final String comment;
@@ -62,13 +63,19 @@ public String getEmail() {
@Override
public String toString() {
- StringBuilder out = new StringBuilder(name);
+ StringBuilder out = new StringBuilder();
- if (comment != null)
- out.append(" (").append(comment).append(')');
+ if (name == null && comment == null && email != null) {
+ out.append(email);
+ }
+ else if (name != null) {
+ out.append(name);
+ if (comment != null)
+ out.append(" (").append(comment).append(')');
- if (email != null)
- out.append(" <").append(email).append('>');
+ if (email != null)
+ out.append(" <").append(email).append('>');
+ }
return out.toString();
}
@@ -96,6 +103,15 @@ public static PGPUserID parse(String uid) {
}
}
+ // try again with email only
+ match = PATTERN_UID_EMAIL_ONLY.matcher(uid);
+ while (match.find()) {
+ if (match.groupCount() >= 1) {
+ String email = match.group(1);
+ return new PGPUserID(null, null, email);
+ }
+ }
+
// no match found
return null;
}
diff --git a/app/src/main/java/org/kontalk/crypto/PersonalKey.java b/app/src/main/java/org/kontalk/crypto/PersonalKey.java
index 661314cc7..df6e8fda5 100644
--- a/app/src/main/java/org/kontalk/crypto/PersonalKey.java
+++ b/app/src/main/java/org/kontalk/crypto/PersonalKey.java
@@ -126,24 +126,12 @@ public String getFingerprint() {
return PGP.getFingerprint(mPair.authKey.getPublicKey());
}
- public PGPKeyPairRing storeNetwork(String userId, String network, String name, String passphrase) throws PGPException, IOException {
- return store(name, userId + '@' + network, null, passphrase);
+ public PGPKeyPairRing storeNetwork(String userId, String network, String passphrase) throws PGPException, IOException {
+ return store(null, userId + '@' + network, null, passphrase);
}
public PGPKeyPairRing store(String name, String email, String comment, String passphrase) throws PGPException, IOException {
- // name[ (comment)] <[email]>
- StringBuilder userid = new StringBuilder(name);
-
- if (comment != null) userid
- .append(" (")
- .append(comment)
- .append(')');
-
- userid.append(" <");
- if (email != null)
- userid.append(email);
- userid.append('>');
-
+ PGPUserID userid = new PGPUserID(name, comment, email);
return PGP.store(mPair, userid.toString(), passphrase);
}
diff --git a/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java b/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java
index 466bf9c20..2c0a685ef 100644
--- a/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java
+++ b/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java
@@ -48,7 +48,7 @@
public class PersonalKeyExporter implements PersonalKeyPack {
public void save(byte[] privateKey, byte[] publicKey, OutputStream dest, String passphrase, String exportPassphrase, byte[] bridgeCert,
- Map trustedKeys, String phoneNumber)
+ Map trustedKeys, String phoneNumber, String displayName)
throws PGPException, IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {
// put everything in a zip file
@@ -127,6 +127,7 @@ public void save(byte[] privateKey, byte[] publicKey, OutputStream dest, String
// export account info
Properties info = new Properties();
info.setProperty("phoneNumber", phoneNumber);
+ info.setProperty("displayName", displayName);
zip.putNextEntry(new ZipEntry(ACCOUNT_INFO_FILENAME));
info.store(zip, null);
zip.closeEntry();
diff --git a/app/src/main/java/org/kontalk/data/Contact.java b/app/src/main/java/org/kontalk/data/Contact.java
index 90c260cb4..661069bc8 100644
--- a/app/src/main/java/org/kontalk/data/Contact.java
+++ b/app/src/main/java/org/kontalk/data/Contact.java
@@ -30,6 +30,7 @@
import com.amulyakhare.textdrawable.util.ColorGenerator;
import org.bouncycastle.util.Arrays;
+import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.jxmpp.util.XmppStringUtils;
@@ -62,7 +63,6 @@
import org.kontalk.crypto.PGPLazyPublicKeyRingLoader;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MessagesProviderClient;
-import org.kontalk.provider.MyUsers.Keys;
import org.kontalk.provider.MyUsers.Users;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.MessageUtils;
@@ -133,6 +133,9 @@ public class Contact {
/** Cached name information from system contacts. It will override our internal name. */
private StructuredName mStructuredName;
+ /** Supported security flags. Detected by {@link org.kontalk.crypto.Coder}s. */
+ private int mSecurityFlags = -1;
+
private static final class StructuredName {
public final String displayName;
public final String givenName;
@@ -505,6 +508,14 @@ public void setLastSeen(long lastSeen) {
mLastSeen = lastSeen;
}
+ public int getSecurityFlags() {
+ return mSecurityFlags;
+ }
+
+ public void setSecurityFlags(int securityFlags) {
+ mSecurityFlags = securityFlags;
+ }
+
public boolean isSelf() {
try {
return Kontalk.get().getDefaultAccount().isSelfJID(JidCreate.bareFrom(mJID));
@@ -765,9 +776,9 @@ public static Contact findByUserId(Context context, @NonNull String userId, Stri
private static void retrieveKeyInfo(Context context, Contact c) {
// trusted key
- Keyring.TrustedPublicKeyData trustedKeyring = Keyring.getPublicKeyData(context, c.getJID(), Keys.TRUST_IGNORED);
+ Keyring.TrustedPublicKeyData trustedKeyring = Keyring.getPublicKeyData(context, c.getJID(), Keyring.TRUST_IGNORED);
// latest (possibly unknown) fingerprint
- c.mFingerprint = Keyring.getFingerprint(context, c.getJID(), Keys.TRUST_UNKNOWN);
+ c.mFingerprint = Keyring.getFingerprint(context, c.getJID(), Keyring.TRUST_UNKNOWN);
if (trustedKeyring != null) {
c.mTrustedKeyRing = new PGPLazyPublicKeyRingLoader(trustedKeyring.keyData);
c.mTrustedLevel = trustedKeyring.trustLevel;
@@ -857,6 +868,21 @@ private static byte[] loadAvatarData(Context context, Uri contactUri) {
return data;
}
+ public static int[] getSecurityFlags(Jid[] users) {
+ final int[] flags = new int[users.length];
+ for (int i = 0; i < users.length; i++) {
+ // hit the cache directly since security flags are not in the database
+ Contact contact = cache.get(users[i].toString());
+ if (contact == null) {
+ flags[i] = -1;
+ }
+ else {
+ flags[i] = contact.getSecurityFlags();
+ }
+ }
+ return flags;
+ }
+
public static Cursor queryContacts(Context context) {
String selection = Users.REGISTERED + " <> 0";
if (!Preferences.getShowBlockedUsers(context)) {
diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java
index 0a32553f7..74ab31640 100644
--- a/app/src/main/java/org/kontalk/provider/Keyring.java
+++ b/app/src/main/java/org/kontalk/provider/Keyring.java
@@ -24,6 +24,9 @@
import java.util.Iterator;
import java.util.Map;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jxmpp.jid.Jid;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -34,11 +37,14 @@
import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;
+import org.kontalk.Log;
import org.kontalk.client.EndpointServer;
import org.kontalk.crypto.Coder;
+import org.kontalk.crypto.OmemoCoder;
import org.kontalk.crypto.PGP;
import org.kontalk.crypto.PGPCoder;
import org.kontalk.crypto.PersonalKey;
+import org.kontalk.data.Contact;
/**
@@ -47,6 +53,12 @@
*/
public class Keyring {
+ private static final String TAG = Keyring.class.getSimpleName();
+
+ public static final int TRUST_UNKNOWN = 0;
+ public static final int TRUST_IGNORED = 1;
+ public static final int TRUST_VERIFIED = 2;
+
/**
* Special value used in the fingerprint column so the first key that comes
* in is automatically trusted.
@@ -58,29 +70,87 @@ private Keyring() {
}
/** Returns a {@link Coder} instance for encrypting data. */
- public static Coder getEncryptCoder(Context context, EndpointServer server, PersonalKey key, String[] recipients) {
- // get recipients public keys from users database
- PGPPublicKeyRing[] keys = new PGPPublicKeyRing[recipients.length];
- for (int i = 0; i < recipients.length; i++) {
- PGPPublicKeyRing ring = getPublicKey(context, recipients[i], MyUsers.Keys.TRUST_UNKNOWN);
- if (ring == null)
- throw new IllegalArgumentException("public key not found for user " + recipients[i]);
+ public static Coder getEncryptCoder(Context context, int securityFlags,
+ XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients)
+ throws SmackException.NotConnectedException {
+
+ // calculate supported security flags given previous discoveries
+ int[] supportedFlags = Contact.getSecurityFlags(recipients);
+ securityFlags = Coder.getCompatibleSecurityFlags(securityFlags, supportedFlags);
+
+ if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) {
+ try {
+ if (recipients.length == 1 && recipients[0].equals(connection.getUser().asBareJid())) {
+ throw new IllegalArgumentException("OMEMO with yourself is not supported");
+ }
+ return new OmemoCoder(connection, getTrustedRecipients(context, recipients));
+ }
+ catch (SmackException.NotConnectedException e) {
+ // not connected to the server, notify to the caller
+ // so it can skip the message and let it retry later
+ throw e;
+ }
+ catch (Exception e) {
+ Log.w(TAG, "unable to setup advanced coder, falling back to basic", e);
+ securityFlags = Coder.SECURITY_BASIC;
+ }
+ }
- keys[i] = ring;
+ // used also as fallback
+ if ((securityFlags & Coder.SECURITY_BASIC) != 0) {
+ // get recipients public keys from users database
+ PGPPublicKeyRing[] keys = new PGPPublicKeyRing[recipients.length];
+ for (int i = 0; i < recipients.length; i++) {
+ PGPPublicKeyRing ring = getPublicKey(context, recipients[i].toString(), Keyring.TRUST_UNKNOWN);
+ if (ring == null)
+ throw new IllegalArgumentException("public key not found for user " + recipients[i]);
+
+ keys[i] = ring;
+ }
+
+ return new PGPCoder(server, key, keys);
}
- return new PGPCoder(server, key, keys);
+ else {
+ throw new IllegalArgumentException("Invalid security flags. No Coder found.");
+ }
+ }
+
+ private static OmemoCoder.TrustedRecipient[] getTrustedRecipients(Context context, Jid[] recipients) {
+ OmemoCoder.TrustedRecipient[] trustedRecipients = new OmemoCoder.TrustedRecipient[recipients.length];
+ for (int i = 0; i < recipients.length; i++) {
+ TrustedPublicKeyData keyInfo = getPublicKeyData(context, recipients[i].asBareJid().toString(), TRUST_UNKNOWN);
+ trustedRecipients[i] = new OmemoCoder.TrustedRecipient(recipients[i], keyInfo.trustLevel, keyInfo.manualTrust);
+ }
+ return trustedRecipients;
}
/** Returns a {@link Coder} instance for decrypting data. */
- public static Coder getDecryptCoder(Context context, EndpointServer server, PersonalKey key, String sender) {
- PGPPublicKeyRing senderKey = getPublicKey(context, sender, MyUsers.Keys.TRUST_IGNORED);
- return new PGPCoder(server, key, senderKey);
+ public static Coder getDecryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid sender) {
+ if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) {
+ try {
+ return new OmemoCoder(connection, sender);
+ }
+ catch (Exception e) {
+ Log.w(TAG, "unable to setup advanced coder, falling back to basic", e);
+ securityFlags = Coder.SECURITY_BASIC;
+ }
+ }
+
+ // used also as fallback
+ if ((securityFlags & Coder.SECURITY_BASIC) != 0) {
+ PGPPublicKeyRing senderKey = getPublicKey(context, sender.toString(), Keyring.TRUST_IGNORED);
+ return new PGPCoder(server, key, senderKey);
+ }
+
+ else {
+ throw new IllegalArgumentException("Invalid security flags. No Coder found.");
+ }
}
/** Returns a {@link Coder} instance for verifying data. */
public static Coder getVerifyCoder(Context context, EndpointServer server, String sender) {
- PGPPublicKeyRing senderKey = getPublicKey(context, sender, MyUsers.Keys.TRUST_UNKNOWN);
+ PGPPublicKeyRing senderKey = getPublicKey(context, sender, Keyring.TRUST_UNKNOWN);
return new PGPCoder(server, null, senderKey);
}
@@ -297,18 +367,6 @@ public static Map fromTrustedFingerprintMap(Map toTrustedFingerprintMap(Map props) {
- Map keys = new HashMap<>(props.size());
- for (Map.Entry e : props.entrySet()) {
- TrustedFingerprint fingerprint = e.getValue();
- if (fingerprint != null) {
- keys.put(e.getKey(), e.toString());
- }
- }
- return keys;
- }
-
public static final class TrustedFingerprint {
public final String fingerprint;
public final int trustLevel;
@@ -327,7 +385,7 @@ public static TrustedFingerprint fromString(String value) {
if (!TextUtils.isEmpty(value)) {
String[] parsed = value.split("\\|");
String fingerprint = parsed[0];
- int trustLevel = MyUsers.Keys.TRUST_UNKNOWN;
+ int trustLevel = Keyring.TRUST_UNKNOWN;
if (parsed.length > 1) {
String _trustLevel = parsed[1];
try {
diff --git a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java
index 4f8149c68..343340cb1 100644
--- a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java
+++ b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java
@@ -106,7 +106,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI
// of course outgoing messages are not encrypted in database
values.put(Messages.ENCRYPTED, false);
values.put(Threads.ENCRYPTION, encrypted);
- values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
if (inReplyTo > 0)
values.put(Messages.IN_REPLY_TO, inReplyTo);
return context.getContentResolver().insert(
@@ -129,7 +129,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI
values.put(Messages.UNREAD, false);
// of course outgoing messages are not encrypted in database
values.put(Messages.ENCRYPTED, false);
- values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
values.put(Messages.DIRECTION, Messages.DIRECTION_OUT);
values.put(Messages.TIMESTAMP, System.currentTimeMillis());
values.put(Messages.STATUS, Messages.STATUS_QUEUED);
@@ -141,7 +141,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI
values.put(Messages.ATTACHMENT_LOCAL_URI, uri.toString());
values.put(Messages.ATTACHMENT_LENGTH, length);
values.put(Messages.ATTACHMENT_COMPRESS, compress);
- values.put(Messages.ATTACHMENT_SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.ATTACHMENT_SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
return context.getContentResolver().insert(Messages.CONTENT_URI, values);
}
@@ -171,7 +171,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI
// of course outgoing messages are not encrypted in database
values.put(Messages.ENCRYPTED, false);
values.put(Threads.ENCRYPTION, encrypted);
- values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
return context.getContentResolver().insert(
Messages.CONTENT_URI, values);
}
@@ -367,6 +367,11 @@ public MessageUpdater setServerTimestamp(long timestamp) {
return this;
}
+ public MessageUpdater setSecurityFlags(int securityFlags) {
+ mValues.put(Messages.SECURITY_FLAGS, securityFlags);
+ return this;
+ }
+
public MessageUpdater appendWhere(String where) {
mWhere = DatabaseUtils.concatenateWhere(mWhere, where);
return this;
@@ -461,7 +466,7 @@ public static void setThreadSticky(Context context, long id, boolean sticky) {
public static int retryMessage(Context context, Uri uri, boolean encrypted) {
ContentValues values = new ContentValues(2);
values.put(Messages.STATUS, Messages.STATUS_SENDING);
- values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
return context.getContentResolver().update(uri, values, null, null);
}
@@ -500,7 +505,7 @@ public static int retryAllMessages(Context context) {
boolean encrypted = Preferences.getEncryptionEnabled(context);
ContentValues values = new ContentValues(2);
values.put(Messages.STATUS, Messages.STATUS_SENDING);
- values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT);
+ values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT);
return context.getContentResolver().update(Messages.CONTENT_URI, values,
Messages.STATUS + "=" + Messages.STATUS_PENDING,
null);
diff --git a/app/src/main/java/org/kontalk/provider/MyUsers.java b/app/src/main/java/org/kontalk/provider/MyUsers.java
index 0a9259766..2066fcae1 100644
--- a/app/src/main/java/org/kontalk/provider/MyUsers.java
+++ b/app/src/main/java/org/kontalk/provider/MyUsers.java
@@ -99,10 +99,6 @@ public static Uri getUri(String jid, String fingerprint) {
.appendPath(fingerprint).build();
}
- public static final int TRUST_UNKNOWN = 0;
- public static final int TRUST_IGNORED = 1;
- public static final int TRUST_VERIFIED = 2;
-
public static final String INSERT_ONLY = "insertOnly";
}
}
diff --git a/app/src/main/java/org/kontalk/service/DownloadService.java b/app/src/main/java/org/kontalk/service/DownloadService.java
index 1135dc568..dd09b7858 100644
--- a/app/src/main/java/org/kontalk/service/DownloadService.java
+++ b/app/src/main/java/org/kontalk/service/DownloadService.java
@@ -31,6 +31,7 @@
import java.util.Map;
import org.greenrobot.eventbus.EventBus;
+import org.jxmpp.jid.impl.JidCreate;
import android.app.Notification;
import android.app.NotificationManager;
@@ -285,25 +286,23 @@ public void completed(String url, String mime, File destination) {
try {
EndpointServer server = Kontalk.get().getEndpointServer();
PersonalKey key = Kontalk.get().getPersonalKey();
- Coder coder = Keyring.getDecryptCoder(this, server, key, mPeer);
- if (coder != null) {
- in = new FileInputStream(destination);
+ Coder coder = Keyring.getDecryptCoder(this, Coder.SECURITY_BASIC, null, server, key, JidCreate.fromOrThrowUnchecked(mPeer));
+ in = new FileInputStream(destination);
- File outFile = new File(destination + ".new");
- out = new FileOutputStream(outFile);
- List errors = new LinkedList<>();
- coder.decryptFile(in, true, out, errors);
+ File outFile = new File(destination + ".new");
+ out = new FileOutputStream(outFile);
+ List errors = new LinkedList<>();
+ coder.decryptFile(in, true, out, errors);
- // TODO process errors
+ // TODO process errors
- // delete old file and rename the decrypted one
- destination.delete();
- outFile.renameTo(destination);
+ // delete old file and rename the decrypted one
+ destination.delete();
+ outFile.renameTo(destination);
- // save this for later
- destinationEncrypted = false;
- destinationLength = destination.length();
- }
+ // save this for later
+ destinationEncrypted = false;
+ destinationLength = destination.length();
}
catch (Exception e) {
Log.e(TAG, "decryption failed!", e);
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java b/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java
deleted file mode 100644
index 9c174225a..000000000
--- a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Kontalk Android client
- * Copyright (C) 2020 Kontalk Devteam
-
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.kontalk.service.msgcenter;
-
-import java.util.List;
-
-import org.jivesoftware.smack.XMPPConnection;
-import org.jivesoftware.smack.filter.StanzaFilter;
-import org.jivesoftware.smack.filter.StanzaIdFilter;
-import org.jivesoftware.smack.packet.Stanza;
-import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
-import org.jivesoftware.smackx.disco.packet.DiscoverItems;
-
-import org.kontalk.Log;
-import org.kontalk.client.EndpointServer;
-import org.kontalk.client.HTTPFileUpload;
-import org.kontalk.client.PushRegistration;
-import org.kontalk.upload.HTTPFileUploadService;
-
-
-/**
- * Packet listener for service discovery (info).
- * @author Daniele Ricci
- */
-class DiscoverInfoListener extends MessageCenterPacketListener {
-
- public DiscoverInfoListener(MessageCenterService instance) {
- super(instance);
- }
-
- @Override
- public void processStanza(Stanza packet) {
- XMPPConnection conn = getConnection();
- EndpointServer server = getServer();
-
- // we don't need this listener anymore
- conn.removeAsyncStanzaListener(this);
-
- DiscoverInfo query = (DiscoverInfo) packet;
- List features = query.getFeatures();
- for (DiscoverInfo.Feature feat : features) {
-
- /*
- * TODO do not request info about push if disabled by user.
- * Of course if user enables push notification we should
- * reissue this discovery again.
- */
- if (PushRegistration.NAMESPACE.equals(feat.getVar())) {
- // push notifications are enabled on this server
- // request items to check if gcm is supported and obtain the server id
- DiscoverItems items = new DiscoverItems();
- items.setNode(PushRegistration.NAMESPACE);
- items.setTo(server.getNetwork());
-
- StanzaFilter filter = new StanzaIdFilter(items.getStanzaId());
- conn.addAsyncStanzaListener(new PushDiscoverItemsListener(getInstance()), filter);
-
- sendPacket(items);
- }
-
- /*
- * TODO upload info should be requested only when needed and
- * cached. This discovery should of course be issued before any
- * media message gets requeued.
- * Actually, delay any message from being requeued if at least
- * 1 media message is present; do the discovery first.
- */
- else if (HTTPFileUpload.NAMESPACE.equals(feat.getVar())) {
- Log.d(MessageCenterService.TAG, "got upload service: " + packet.getFrom());
- addUploadService(new HTTPFileUploadService(conn, packet.getFrom().asBareJid()), 0);
- // MessagesController will send pending messages
- }
- }
- }
-}
-
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java b/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java
deleted file mode 100644
index 20693f04d..000000000
--- a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Kontalk Android client
- * Copyright (C) 2020 Kontalk Devteam
-
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.kontalk.service.msgcenter;
-
-import java.util.List;
-
-import org.jivesoftware.smack.XMPPConnection;
-import org.jivesoftware.smack.filter.StanzaFilter;
-import org.jivesoftware.smack.filter.StanzaIdFilter;
-import org.jivesoftware.smack.packet.Stanza;
-import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
-import org.jivesoftware.smackx.disco.packet.DiscoverItems;
-
-
-/**
- * Packet listener for service discovery (items).
- * @author Daniele Ricci
- */
-class DiscoverItemsListener extends MessageCenterPacketListener {
-
- public DiscoverItemsListener(MessageCenterService instance) {
- super(instance);
- }
-
- @Override
- public void processStanza(Stanza packet) {
- XMPPConnection conn = getConnection();
-
- // we don't need this listener anymore
- conn.removeAsyncStanzaListener(this);
-
- DiscoverItems query = (DiscoverItems) packet;
- List items = query.getItems();
- for (DiscoverItems.Item item : items) {
- DiscoverInfo info = new DiscoverInfo();
- info.setTo(item.getEntityID());
-
- StanzaFilter filter = new StanzaIdFilter(info.getStanzaId());
- conn.addAsyncStanzaListener(new DiscoverInfoListener(getInstance()), filter);
- sendPacket(info);
- }
- }
-}
-
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java b/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java
index ae59b0ea0..3e0c449aa 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java
@@ -45,9 +45,9 @@ public void processStanza(Stanza packet) {
public void processException(Exception exception) {
if (exception instanceof XMPPException.XMPPErrorException) {
String id = ((XMPPException.XMPPErrorException) exception)
- .getStanzaError().getStanza().getStanzaId();
+ .getRequest().getStanzaId();
Jid jid = ((XMPPException.XMPPErrorException) exception)
- .getStanzaError().getStanza().getFrom();
+ .getRequest().getFrom();
MessageCenterService.bus()
.post(new LastActivityEvent(exception, jid, id));
}
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java
index 07f5aecdc..9ab609546 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java
@@ -130,43 +130,6 @@ protected boolean sendMessage(Message message, long databaseId) {
return instance != null && instance.sendMessage(message, databaseId);
}
- protected void addUploadService(IUploadService service) {
- MessageCenterService instance = mInstance.get();
- if (instance != null)
- instance.addUploadService(service);
- }
-
- protected void addUploadService(IUploadService service, int priority) {
- MessageCenterService instance = mInstance.get();
- if (instance != null)
- instance.addUploadService(service, priority);
- }
-
- protected boolean isPushNotificationsEnabled() {
- MessageCenterService instance = mInstance.get();
- return instance != null && instance.mPushNotifications;
- }
-
- protected void setPushSenderId(String senderId) {
- MessageCenterService.sPushSenderId = senderId;
- }
-
- protected IPushListener getPushListener() {
- return MessageCenterService.sPushListener;
- }
-
- protected void startPushRegistrationCycle() {
- MessageCenterService instance = mInstance.get();
- if (instance != null)
- instance.mPushRegistrationCycle = true;
- }
-
- protected void pushRegister() {
- MessageCenterService instance = mInstance.get();
- if (instance != null)
- instance.pushRegister();
- }
-
protected WakefulHashSet getWaitingReceiptList() {
MessageCenterService instance = mInstance.get();
return (instance != null) ? instance.mWaitingReceipt : null;
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java
index 905d52e31..553c9c151 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java
@@ -62,17 +62,18 @@
import org.jivesoftware.smack.util.Async;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.SuccessCallback;
-import org.jivesoftware.smackx.caps.packet.CapsExtension;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
import org.jivesoftware.smackx.csi.ClientStateIndicationManager;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.forward.packet.Forwarded;
import org.jivesoftware.smackx.iqlast.packet.LastActivity;
import org.jivesoftware.smackx.iqversion.VersionManager;
import org.jivesoftware.smackx.iqversion.packet.Version;
+import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.ping.PingFailedListener;
import org.jivesoftware.smackx.ping.PingManager;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
@@ -122,8 +123,8 @@
import org.kontalk.authenticator.MyAccount;
import org.kontalk.client.BitsOfBinary;
import org.kontalk.client.BlockingCommand;
-import org.kontalk.client.E2EEncryption;
import org.kontalk.client.EndpointServer;
+import org.kontalk.client.HTTPFileUpload;
import org.kontalk.client.KontalkConnection;
import org.kontalk.client.OutOfBandData;
import org.kontalk.client.PublicKeyPublish;
@@ -152,7 +153,6 @@
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.provider.MyMessages.Threads.Requests;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.UsersProvider;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.KeyPairGeneratorService;
@@ -196,6 +196,7 @@
import org.kontalk.service.msgcenter.group.SetSubjectCommand;
import org.kontalk.ui.MessagingNotification;
import org.kontalk.util.DataUtils;
+import org.kontalk.upload.HTTPFileUploadService;
import org.kontalk.util.EventBusIndex;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.MessageUtils;
@@ -338,6 +339,7 @@ protected void log(String logMessage, Throwable throwable) {
private WakeLock mPingLock;
LocalBroadcastManager mLocalBroadcastManager;
private AlarmManager mAlarmManager;
+ private OmemoManager mOmemoManager;
private LastActivityListener mLastActivityListener;
private PingFailedListener mPingFailedListener;
@@ -979,9 +981,15 @@ private synchronized void quit(boolean restarting) {
mConnection = null;
}
+ if (mOmemoManager != null) {
+ mOmemoManager.stopStanzaAndPEPListeners();
+ }
+
// clear the connection only if we are quitting
- if (!restarting)
+ if (!restarting) {
+ mOmemoManager = null;
mConnection = null;
+ }
}
if (mUploadServices != null) {
@@ -1347,8 +1355,7 @@ public void handleUploadAttachment(UploadAttachmentRequest request) {
encryptTo = new String[] { message.getRecipient() };
}
- File encrypted = MessageUtils.encryptFile(this, in,
- DataUtils.toString(encryptTo));
+ File encrypted = MessageUtils.encryptFile(this, in, XMPPUtils.parseJids(encryptTo));
fileLength = encrypted.length();
preMediaUri = Uri.fromFile(encrypted);
}
@@ -1360,6 +1367,11 @@ public void handleUploadAttachment(UploadAttachmentRequest request) {
fileLength = MediaStorage.getLength(this, preMediaUri);
}
}
+ catch (SmackException.NotConnectedException e) {
+ // will retry at next reconnection
+ Log.w(TAG, "not connected, encryption failed, will send message later", e);
+ return;
+ }
catch (Exception e) {
Log.w(TAG, "error preprocessing media: " + preMediaUri, e);
// simulate upload error
@@ -1742,7 +1754,18 @@ public synchronized void created(final XMPPConnection connection) {
filter = new StanzaTypeFilter(Presence.class);
connection.addAsyncStanzaListener(presenceListener, filter);
- filter = new StanzaTypeFilter(org.jivesoftware.smack.packet.Message.class);
+ filter = new StanzaFilter() {
+ @Override
+ public boolean accept(Stanza stanza) {
+ // ignore OMEMO messages, they will be processed by smack-omemo
+ // except delayed messages which must be processed manually via decrypt()
+ // .......ARGH!!!
+ // FIXME is this still applicable to 4.4.0?
+ return stanza instanceof org.jivesoftware.smack.packet.Message/* &&
+ (!OmemoManager.stanzaContainsOmemoElement(stanza) ||
+ stanza.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE))*/;
+ }
+ };
connection.addSyncStanzaListener(new MessageListener(this), filter);
// this is used as a reply callback
@@ -1830,13 +1853,38 @@ public void run() {
final XMPPConnection conn = mConnection;
if (conn != null && conn.isConnected()) {
Jid jid = conn.getXMPPServiceDomain();
- if (Keyring.getPublicKey(MessageCenterService.this, jid.toString(), MyUsers.Keys.TRUST_UNKNOWN) == null) {
+ if (Keyring.getPublicKey(MessageCenterService.this, jid.toString(), Keyring.TRUST_UNKNOWN) == null) {
BUS.post(new PublicKeyRequest(jid));
}
}
}
});
+ boolean supported;
+ try {
+ supported = OmemoManager.serverSupportsOmemo(mConnection,
+ mConnection.getXMPPServiceDomain());
+ }
+ catch (Exception e) {
+ supported = false;
+ Log.w(TAG, "unable to determine server support for OMEMO", e);
+ ReportingManager.logException(e);
+ }
+
+ if (supported) {
+ // we logged in so we can now initialize OMEMO
+ mOmemoManager = OmemoManager.getInstanceFor(connection);
+ mOmemoManager.resumeStanzaAndPEPListeners();
+ try {
+ mOmemoManager.initialize();
+ }
+ catch (Exception e) {
+ Log.w(TAG, "unable to initialize OMEMO engine", e);
+ ReportingManager.logException(e);
+ mOmemoManager = null;
+ }
+ }
+
// re-acquire the wakelock for a limited time to allow for messages to come
// it will then be released automatically
mWakeLock.acquire(WAIT_FOR_MESSAGES_DELAY);
@@ -1860,30 +1908,105 @@ void broadcast(String action, String extraName, String extraValue) {
* Discovers info and items.
*/
private void discovery() {
- StanzaFilter filter;
+ // FIXME some messed up, absolutely unmodular code here
- Jid to;
- try {
- to = JidCreate.domainBareFrom(mServer.getNetwork());
- }
- catch (XmppStringprepException e) {
- Log.w(TAG, "error parsing JID: " + e.getCausingString(), e);
- // report it because it's a big deal
- ReportingManager.logException(e);
- return;
+ final KontalkConnection connection = mConnection;
+ if (connection != null) {
+ Async.go(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ DiscoverInfo info = connection.getDiscoverInfo();
+
+ if (info.containsFeature(PushRegistration.NAMESPACE)) {
+ discoverPushRegistration(connection);
+ }
+ }
+ catch (SmackException.NoResponseException e) {
+ Log.w(TAG, "No response for discovery of info was received", e);
+ }
+ catch (XMPPException.XMPPErrorException e) {
+ Log.w(TAG, "Error requesting discovery of info", e);
+ }
+ catch (NotConnectedException ignored) {
+ }
+ catch (InterruptedException ignored) {
+ }
+ }
+ }, "ServerDiscoveryInfo");
+
+ Async.go(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ List items = manager.
+ discoverItems(connection.getXMPPServiceDomain()).getItems();
+
+ for (DiscoverItems.Item item : items) {
+ DiscoverInfo info = manager.discoverInfo(item.getEntityID());
+
+ if (info.containsFeature(HTTPFileUpload.NAMESPACE)) {
+ Log.d(MessageCenterService.TAG, "got upload service: " + item.getEntityID());
+ addUploadService(new HTTPFileUploadService(connection,
+ item.getEntityID().asBareJid()), 0);
+ // MessagesController will send pending messages
+ }
+ }
+ }
+ catch (SmackException.NoResponseException e) {
+ Log.w(TAG, "No response for discovery of items was received", e);
+ }
+ catch (XMPPException.XMPPErrorException e) {
+ Log.w(TAG, "Error requesting discovery of items", e);
+ }
+ catch (NotConnectedException ignored) {
+ }
+ catch (InterruptedException ignored) {
+ }
+ }
+ }, "ServerDiscoveryItems");
}
+ }
- DiscoverInfo info = new DiscoverInfo();
- info.setTo(to);
- filter = new StanzaIdFilter(info.getStanzaId());
- mConnection.addAsyncStanzaListener(new DiscoverInfoListener(this), filter);
- sendPacket(info);
+ /** For call by {@link #discovery()} listeners. */
+ void discoverPushRegistration(XMPPConnection connection) throws XMPPException.XMPPErrorException,
+ NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(connection);
+ List items = manager.discoverItems(connection.getXMPPServiceDomain(),
+ PushRegistration.NAMESPACE).getItems();
+
+ for (DiscoverItems.Item item : items) {
+ String jid = item.getEntityID().toString();
+ // google push notifications
+ if (("gcm.push." + connection.getXMPPServiceDomain().toString()).equals(jid)) {
+ String senderId = item.getNode();
+ MessageCenterService.sPushSenderId = senderId;
+
+ if (mPushNotifications) {
+ String oldSender = Preferences.getPushSenderId();
+
+ // store the new sender id
+ Preferences.setPushSenderId(senderId);
+
+ // begin a registration cycle if senderId is different
+ if (oldSender != null && !oldSender.equals(senderId)) {
+ IPushService service = PushServiceManager.getInstance(this);
+ if (service != null)
+ service.unregister(sPushListener);
+ // unregister will see this as an attempt to register again
+ mPushRegistrationCycle = true;
+ }
+ else {
+ // begin registration immediately
+ pushRegister();
+ }
+ }
+ }
+ }
- DiscoverItems items = new DiscoverItems();
- items.setTo(to);
- filter = new StanzaIdFilter(items.getStanzaId());
- mConnection.addAsyncStanzaListener(new DiscoverItemsListener(this), filter);
- sendPacket(items);
}
synchronized void active(boolean available) {
@@ -1976,10 +2099,6 @@ private Presence createPresence(Presence.Mode mode) {
p.setStatus(status);
if (mode != null)
p.setMode(mode);
-
- // TODO find a place for this
- p.addExtension(new CapsExtension("http://www.kontalk.org/", "none", "sha-1"));
-
return p;
}
@@ -2371,6 +2490,9 @@ else if (groupCmdComponent.isPartCommand()) {
attachment.getMime(), attachment.getLength(),
attachment.getSecurityFlags() != Coder.SECURITY_CLEARTEXT));
}
+
+ // fall back to basic until we'll have a XEP for this
+ message.setSecurityFlags(Coder.SECURITY_BASIC);
}
// add location data if present
@@ -2381,6 +2503,9 @@ else if (groupCmdComponent.isPartCommand()) {
UserLocation userLocation = new UserLocation(lat, lon,
location.getText(), location.getStreet());
m.addExtension(userLocation);
+
+ // fall back to basic until we'll have a XEP for this
+ message.setSecurityFlags(Coder.SECURITY_BASIC);
}
// add referenced message if any
@@ -2408,38 +2533,33 @@ else if (groupCmdComponent.isPartCommand()) {
}
if (message.getSecurityFlags() != Coder.SECURITY_CLEARTEXT) {
- byte[] toMessage = null;
boolean encryptError = false;
try {
- Coder coder = Keyring.getEncryptCoder(this, mServer, key, DataUtils.toString(toGroup));
- if (coder != null) {
-
- // no extensions, create a simple text version to save space
- if (msg.getExtensions().size() == 0) {
- if (!(request instanceof SendDeliveryReceiptRequest)) {
- // a special case for delivery receipts whom doesn't have a body
- // but we want to encrypt it for groups (extensions.size() > 0)
- toMessage = coder.encryptText(msg.getBody());
- }
- }
+ Coder coder = Keyring.getEncryptCoder(this, message.getSecurityFlags(),
+ mConnection, mServer, key, toGroup);
+ // cache security flags
+ for (Jid jid : toGroup) {
+ Contact.findByUserId(this, jid.asBareJid().toString())
+ .setSecurityFlags(coder.getSupportedFlags());
+ }
- // some extension, encrypt whole stanza just to be sure
- else {
- toMessage = coder.encryptStanza(msg.toXML(null));
- }
+ // security flags changed (most probably because of coder fallback)
+ // update message accordingly
+ if ((message.getSecurityFlags() & coder.getSupportedFlags()) == 0) {
+ message.setSecurityFlags(message.getSecurityFlags() | coder.getSupportedFlags());
+ MessagesProviderClient.MessageUpdater.forMessage(this, message.getDatabaseId())
+ .setSecurityFlags(message.getSecurityFlags())
+ .commit();
+ }
- if (toMessage != null) {
- org.jivesoftware.smack.packet.Message encMsg =
- new org.jivesoftware.smack.packet.Message(msg.getTo(), msg.getType());
+ org.jivesoftware.smack.packet.Message encMsg = null;
- encMsg.setBody(getString(R.string.text_encrypted));
- encMsg.setStanzaId(m.getStanzaId());
- encMsg.addExtension(new E2EEncryption(toMessage));
+ if (!(request instanceof SendDeliveryReceiptRequest && groupController == null)) {
+ encMsg = coder.encryptMessage(msg, getString(R.string.text_encrypted));
+ }
- // save the unencrypted stanza for later
- originalStanza = msg;
- m = encMsg;
- }
+ if (encMsg != null) {
+ m = encMsg;
}
}
@@ -2455,6 +2575,8 @@ else if (groupCmdComponent.isPartCommand()) {
encryptError = true;
}
catch (GeneralSecurityException e) {
+ Log.w(TAG, "encryption failed!", e);
+
// warn user: message will not be sent
if (MessagingNotification.isPaused(convJid)) {
Toast.makeText(this, R.string.warn_encryption_failed,
@@ -2462,6 +2584,12 @@ else if (groupCmdComponent.isPartCommand()) {
}
encryptError = true;
}
+ catch (SmackException.NotConnectedException e) {
+ // will retry at next reconnection
+ Log.w(TAG, "not connected, encryption failed, will send message later", e);
+ mIdleHandler.release();
+ return;
+ }
if (encryptError) {
// message was not encrypted for some reason, mark it pending user review
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java
index edc3f1403..b6d88e0b9 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java
@@ -22,13 +22,21 @@
import java.io.IOException;
import java.util.Date;
+import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
import org.jivesoftware.smackx.forward.packet.Forwarded;
+import org.jivesoftware.smackx.omemo.OmemoManager;
+import org.jivesoftware.smackx.omemo.OmemoMessage;
+import org.jivesoftware.smackx.omemo.element.OmemoElement;
+import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
+import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
import org.jxmpp.jid.Jid;
@@ -83,17 +91,29 @@
import org.kontalk.util.Preferences;
import org.kontalk.util.XMPPUtils;
-import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_INVALID_TIMESTAMP;
-
/**
* Packet listener for message stanzas.
* @author Daniele Ricci
*/
-class MessageListener extends WakefulMessageCenterPacketListener {
+class MessageListener extends WakefulMessageCenterPacketListener implements ConnectionListener, OmemoMessageListener {
+
+ private OmemoManager mOmemoManager;
public MessageListener(MessageCenterService instance) {
super(instance, "RECV");
+ getConnection().addConnectionListener(this);
+ }
+
+ @Override
+ public void authenticated(XMPPConnection connection, boolean resumed) {
+ // Because of the way the smack-omemo is designed, we can't separate
+ // incoming message handling from decryption.
+ // We'll have a local OmemoManager to inject decrypted messages in the
+ // incoming message processing workflow.
+ mOmemoManager = OmemoManager.getInstanceFor(connection);
+ mOmemoManager.removeOmemoMessageListener(this);
+ mOmemoManager.addOmemoMessageListener(this);
}
private static final class GroupMessageProcessingResult {
@@ -241,6 +261,29 @@ private ChatStateEvent processChatState(Message m) {
return null;
}
+ /**
+ * Process an incoming OMEMO message.
+ */
+ @Override
+ public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) {
+ // duplicates the message to fool real processing
+ Message output = ((Message) stanza).asBuilder()
+ // TODO ignoring other decrypted message information
+ .setBody(decryptedMessage.getBody())
+ .build();
+
+ try {
+ processWakefulStanza(output);
+ }
+ catch (SmackException.NotConnectedException ignored) {
+ }
+ }
+
+ @Override
+ public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoMessage.Received decryptedCarbonCopy) {
+ // TODO will be used one day
+ }
+
/**
* Process an incoming message packet.
* @param m the message
@@ -298,78 +341,102 @@ private ChatStateEvent processChatMessage(Message m, @Nullable ChatStateEvent ch
// ack request might not be encrypted
boolean needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE);
- ExtensionElement _encrypted = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE);
+ try {
+ Coder coder = null;
+ int securityFlags = 0;
+ boolean verifyOnly = false;
- if (_encrypted instanceof E2EEncryption) {
- E2EEncryption mEnc = (E2EEncryption) _encrypted;
- byte[] encryptedData = mEnc.getData();
+ if (m.hasExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE)) {
+ securityFlags = Coder.SECURITY_BASIC;
+ }
- // encrypted message
- msg.setEncrypted(true);
- msg.setSecurityFlags(Coder.SECURITY_BASIC);
+ else if (m.hasExtension(OmemoElement.NAME_ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) {
+ securityFlags = Coder.SECURITY_ADVANCED;
+ }
- if (encryptedData != null) {
+ else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE)) {
+ securityFlags = Coder.SECURITY_BASIC_SIGNED;
+ }
+ else {
+ // use message body
+ if (body != null)
+ msg.addComponent(new TextComponent(body));
+ }
- // decrypt message
- try {
- Message innerStanza = decryptMessage(msg, encryptedData);
- if (innerStanza != null) {
- // copy some attributes over
- innerStanza.setTo(m.getTo());
- innerStanza.setFrom(m.getFrom());
- innerStanza.setType(m.getType());
- m = innerStanza;
-
- if (!needAck) {
- // try the decrypted message
- needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE);
- }
- }
- }
+ if (securityFlags > 0) {
+ Context context = getContext();
+ EndpointServer server = getServer();
+ if (server == null)
+ server = Kontalk.get().getEndpointServer();
- catch (Exception exc) {
- Log.e(MessageCenterService.TAG, "decryption failed", exc);
+ PersonalKey key = Kontalk.get().getPersonalKey();
- // raw component for encrypted data
- // reuse security flags
- msg.clearComponents();
- msg.addComponent(new RawComponent(encryptedData, true, msg.getSecurityFlags()));
+ if ((securityFlags & Coder.SECURITY_ADVANCED_ENCRYPTED) != 0 || (securityFlags & Coder.SECURITY_BASIC_ENCRYPTED) != 0) {
+ coder = Keyring.getDecryptCoder(context, securityFlags, getConnection(),
+ server, key, JidCreate.bareFromOrThrowUnchecked(msg.getSender(true)));
+ }
+ else {
+ coder = Keyring.getVerifyCoder(context, server, msg.getSender(true));
+ verifyOnly = true;
}
-
}
- }
-
- else {
-
- // use message body
- if (body != null)
- msg.addComponent(new TextComponent(body));
-
- // old PGP signature
- ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE);
- if (_pgpSigned instanceof OpenPGPSignedMessage) {
- OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned;
- byte[] signedData = pgpSigned.getData();
- // signed message
- msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED);
-
- if (signedData != null) {
- // check signature
- try {
- checkSignedMessage(msg, pgpSigned.getData());
- // at this point our message should be filled with the verified body
+ if (coder != null) {
+ if (verifyOnly) {
+ // use message body
+ if (body != null)
+ msg.addComponent(new TextComponent(body));
+
+ // old PGP signature
+ ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE);
+ if (_pgpSigned instanceof OpenPGPSignedMessage) {
+ OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned;
+ byte[] signedData = pgpSigned.getData();
+
+ // signed message
+ msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED);
+
+ if (signedData != null) {
+ // check signature
+ try {
+ checkSignedMessage(msg, pgpSigned.getData());
+ // at this point our message should be filled with the verified body
+ }
+
+ catch (Exception exc) {
+ Log.e(MessageCenterService.TAG, "signature check failed", exc);
+ // TODO what to do here?
+ msg.setSecurityFlags(msg.getSecurityFlags() |
+ Coder.SECURITY_ERROR_INVALID_SIGNATURE);
+ }
+ }
}
-
- catch (Exception exc) {
- Log.e(MessageCenterService.TAG, "signature check failed", exc);
- // TODO what to do here?
- msg.setSecurityFlags(msg.getSecurityFlags() |
- Coder.SECURITY_ERROR_INVALID_SIGNATURE);
+ }
+ else {
+ Message innerStanza = decryptMessage(msg, coder, m);
+ // copy some attributes over
+ innerStanza.setTo(m.getTo());
+ innerStanza.setFrom(m.getFrom());
+ innerStanza.setType(m.getType());
+ m = innerStanza;
+
+ if (!needAck) {
+ // try the decrypted message
+ needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE);
}
}
}
-
+ }
+ catch (Exception exc) {
+ Log.e(MessageCenterService.TAG, "decryption failed", exc);
+
+ // raw component for encrypted data
+ // reuse security flags
+ msg.clearComponents();
+ msg.addComponent(new RawComponent(m.toXML().toString().getBytes(), true, msg.getSecurityFlags()));
+ // and body placeholder
+ if (body != null)
+ msg.addComponent(new TextComponent(body));
}
// out of band data
@@ -574,46 +641,21 @@ private void sendReceipt(Uri msgUri, String msgId, Jid from) {
sendMessage(ack, storageId);
}
- private Message decryptMessage(CompositeMessage msg, byte[] encryptedData) throws Exception {
- // message stanza
- Message m = null;
-
+ private Message decryptMessage(CompositeMessage msg, Coder coder, Message packet) throws Exception {
try {
- Context context = getContext();
- PersonalKey key = Kontalk.get().getPersonalKey();
-
- EndpointServer server = getServer();
- if (server == null)
- server = Kontalk.get().getEndpointServer();
-
- Coder coder = Keyring.getDecryptCoder(context, server, key, msg.getSender(true));
-
// decrypt
- Coder.DecryptOutput result = coder.decryptText(encryptedData, true);
-
- String contentText;
-
- if (XMPPUtils.XML_XMPP_TYPE.equalsIgnoreCase(result.mime)) {
- m = XMPPUtils.parseMessageStanza(result.cleartext);
-
- if (result.timestamp != null && !checkDriftedDelay(m, result.timestamp))
- result.errors.add(new DecryptException(DECRYPT_EXCEPTION_INVALID_TIMESTAMP,
- "Drifted timestamp"));
-
- contentText = m.getBody();
- }
- else {
- contentText = result.cleartext;
- }
+ Coder.DecryptOutput result = coder.decryptMessage(packet, true);
// clear components (we are adding new ones)
msg.clearComponents();
// decrypted text
- if (contentText != null)
- msg.addComponent(new TextComponent(contentText));
+ if (result.cleartext != null && result.cleartext.getBody() != null)
+ msg.addComponent(new TextComponent(result.cleartext.getBody()));
- if (result.errors.size() > 0) {
+ // import security flags from coder
+ msg.setSecurityFlags(result.securityFlags);
+ if (result.errors.size() > 0) {
int securityFlags = msg.getSecurityFlags();
for (DecryptException err : result.errors) {
@@ -654,7 +696,7 @@ private Message decryptMessage(CompositeMessage msg, byte[] encryptedData) throw
msg.setEncrypted(false);
- return m;
+ return result.cleartext;
}
catch (Exception exc) {
// pass over the message even if encrypted
@@ -757,17 +799,17 @@ private void checkSignedMessage(CompositeMessage msg, byte[] signedData) throws
}
}
- private static boolean checkDriftedDelay(Message m, Date expected) {
- Date stamp = XMPPUtils.getStanzaDelay(m);
- if (stamp != null) {
- long time = stamp.getTime();
- long now = expected.getTime();
- long diff = Math.abs(now - time);
- return (diff < Coder.TIMEDIFF_THRESHOLD);
- }
+ // methods not used.
+
+ @Override
+ public void connected(XMPPConnection connection) {
+ }
- // no timestamp found
- return true;
+ @Override
+ public void connectionClosed() {
}
+ @Override
+ public void connectionClosedOnError(Exception e) {
+ }
}
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java
index 62178c8cc..d441a9691 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java
@@ -43,7 +43,6 @@
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MessagesProviderClient;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.MyUsers.Users;
import org.kontalk.provider.UsersProvider;
import org.kontalk.service.msgcenter.event.PresenceEvent;
@@ -103,7 +102,7 @@ public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) {
String jid = from.asBareJid().toString();
// store key to users table
- Keyring.setKey(getContext(), jid, keydata, MyUsers.Keys.TRUST_VERIFIED);
+ Keyring.setKey(getContext(), jid, keydata, Keyring.TRUST_VERIFIED);
}
}
catch (Exception e) {
@@ -170,7 +169,7 @@ public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) {
// insert key if any
if (publicKey != null) {
try {
- Keyring.setKey(ctx, fromStr, publicKey, MyUsers.Keys.TRUST_UNKNOWN);
+ Keyring.setKey(ctx, fromStr, publicKey, Keyring.TRUST_UNKNOWN);
}
catch (Exception e) {
Log.w(TAG, "invalid public key from " + fromStr, e);
@@ -214,7 +213,7 @@ public void run() {
boolean requestKey = false;
String jid = p.getFrom().asBareJid().toString();
PGPPublicKeyRing pubRing = Keyring.getPublicKey(getContext(),
- jid, MyUsers.Keys.TRUST_UNKNOWN);
+ jid, Keyring.TRUST_UNKNOWN);
if (pubRing != null) {
String oldFingerprint = PGP.getFingerprint(PGP.getMasterKey(pubRing));
if (!newFingerprint.equalsIgnoreCase(oldFingerprint)) {
@@ -243,7 +242,7 @@ public static PresenceEvent createEvent(Context ctx, Presence p, RosterEntry ent
String jid = p.getFrom().asBareJid().toString();
Date delayTime;
- DelayInformation delay = p.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE);
+ DelayInformation delay = DelayInformation.from(p);
if (delay != null) {
delayTime = delay.getStamp();
}
@@ -259,7 +258,7 @@ public static PresenceEvent createEvent(Context ctx, Presence p, RosterEntry ent
String fingerprint = PublicKeyPresence.getFingerprint(p);
if (fingerprint == null) {
// try untrusted fingerprint from database
- fingerprint = Keyring.getFingerprint(ctx, jid, MyUsers.Keys.TRUST_UNKNOWN);
+ fingerprint = Keyring.getFingerprint(ctx, jid, Keyring.TRUST_UNKNOWN);
}
// subscription information
@@ -306,7 +305,7 @@ int updateUsersDatabase(Presence p) {
// delay
long timestamp;
- DelayInformation delay = p.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE);
+ DelayInformation delay = DelayInformation.from(p);
if (delay != null) {
// delay from presence (rare)
timestamp = delay.getStamp().getTime();
@@ -320,9 +319,9 @@ int updateUsersDatabase(Presence p) {
values.put(Users.LAST_SEEN, timestamp);
// public key extension (for fingerprint)
- PublicKeyPresence pkey = p.getExtension(PublicKeyPresence.ELEMENT_NAME, PublicKeyPresence.NAMESPACE);
- if (pkey != null) {
- String fingerprint = pkey.getFingerprint();
+ ExtensionElement pkey = p.getExtension(PublicKeyPresence.ELEMENT_NAME, PublicKeyPresence.NAMESPACE);
+ if (pkey instanceof PublicKeyPresence) {
+ String fingerprint = ((PublicKeyPresence) pkey).getFingerprint();
if (fingerprint != null) {
// insert new key with empty key data
Keyring.setKey(getContext(), jid, fingerprint, new Date());
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java
index fec16939c..42292fc16 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java
@@ -86,7 +86,7 @@ public void processStanza(Stanza packet) {
return;
}
- DataForm response = iq.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(iq);
if (response == null) {
finish(StanzaError.Condition.internal_server_error);
return;
@@ -133,16 +133,14 @@ private Stanza prepareKeyPacket() {
Form form = new Form(DataForm.Type.submit);
// form type: register#privatekey
- FormField type = new FormField("FORM_TYPE");
- type.setType(FormField.Type.hidden);
- type.addValue("http://kontalk.org/protocol/register#privatekey");
- form.addField(type);
+ form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#privatekey"));
// private key
- FormField fieldKey = new FormField("privatekey");
- fieldKey.setLabel("Private key");
- fieldKey.setType(FormField.Type.text_single);
- fieldKey.addValue(privatekey);
+ FormField fieldKey = FormField.builder("privatekey")
+ .setLabel("Private key")
+ .setType(FormField.Type.text_single)
+ .addValue(privatekey)
+ .build();
form.addField(fieldKey);
iq.addExtension(form.getDataFormToSend());
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java
index 8aa096c12..7c5584773 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java
@@ -33,7 +33,6 @@
import org.kontalk.crypto.X509Bridge;
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.UsersProvider;
import org.kontalk.service.msgcenter.event.PublicKeyEvent;
import org.kontalk.sync.SyncAdapter;
@@ -102,7 +101,7 @@ public void processStanza(Stanza packet) {
Log.v("pubkey", "Updating server key for " + from);
try {
Keyring.setKey(getContext(), from.toString(), _publicKey,
- MyUsers.Keys.TRUST_VERIFIED);
+ Keyring.TRUST_VERIFIED);
}
catch (Exception e) {
// TODO warn user
@@ -114,7 +113,7 @@ public void processStanza(Stanza packet) {
try {
Log.v("pubkey", "Updating key for " + from);
Keyring.setKey(getContext(), from.toString(), _publicKey,
- selfJid ? MyUsers.Keys.TRUST_VERIFIED : -1);
+ selfJid ? Keyring.TRUST_VERIFIED : -1);
// update display name with uid (if empty)
PGPUserID keyUid = PGP.parseUserId(_publicKey, getConnection().getXMPPServiceDomain().toString());
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java
deleted file mode 100644
index 18c0beb13..000000000
--- a/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Kontalk Android client
- * Copyright (C) 2020 Kontalk Devteam
-
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.kontalk.service.msgcenter;
-
-import java.util.List;
-
-import org.jivesoftware.smack.packet.Stanza;
-import org.jivesoftware.smackx.disco.packet.DiscoverItems;
-import org.kontalk.util.Preferences;
-
-
-/**
- * Packet listener for discovering push notifications support.
- * @author Daniele Ricci
- */
-class PushDiscoverItemsListener extends MessageCenterPacketListener {
-
- public PushDiscoverItemsListener(MessageCenterService instance) {
- super(instance);
- }
-
- @Override
- public void processStanza(Stanza packet) {
- // we don't need this listener anymore
- getConnection().removeAsyncStanzaListener(this);
-
- DiscoverItems query = (DiscoverItems) packet;
- List items = query.getItems();
- for (DiscoverItems.Item item : items) {
- String jid = item.getEntityID().toString();
- // google push notifications
- if (("gcm.push." + getServer().getNetwork()).equals(jid)) {
- String senderId = item.getNode();
- setPushSenderId(senderId);
-
- if (isPushNotificationsEnabled()) {
- String oldSender = Preferences.getPushSenderId();
-
- // store the new sender id
- Preferences.setPushSenderId(senderId);
-
- // begin a registration cycle if senderId is different
- if (oldSender != null && !oldSender.equals(senderId)) {
- IPushService service = PushServiceManager.getInstance(getContext());
- if (service != null)
- service.unregister(getPushListener());
- // unregister will see this as an attempt to register again
- startPushRegistrationCycle();
- }
- else {
- // begin registration immediately
- pushRegister();
- }
- }
- }
- }
- }
-}
-
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java
index 9507179ad..bb27f1531 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java
@@ -101,7 +101,7 @@ public void run(PersonalKey key) {
MyAccount account = Kontalk.get().getDefaultAccount();
String userId = XMPPUtils.createLocalpart(account.getName());
- mKeyRing = key.storeNetwork(userId, getServer().getNetwork(), account.getDisplayName(),
+ mKeyRing = key.storeNetwork(userId, getServer().getNetwork(),
// TODO should we ask passphrase to the user?
account.getPassphrase());
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java
index 44125d631..fb7f96472 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java
@@ -97,26 +97,25 @@ private Stanza prepareKeyPacket() {
Form form = new Form(DataForm.Type.submit);
// form type: register#key
- FormField type = new FormField("FORM_TYPE");
- type.setType(FormField.Type.hidden);
- type.addValue("http://kontalk.org/protocol/register#key");
- form.addField(type);
+ form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#key"));
// new (to-be-signed) public key
- FormField fieldKey = new FormField("publickey");
- fieldKey.setLabel("Public key");
- fieldKey.setType(FormField.Type.text_single);
- fieldKey.addValue(publicKey);
+ FormField fieldKey = FormField.builder("publickey")
+ .setLabel("Public key")
+ .setType(FormField.Type.text_single)
+ .addValue(publicKey)
+ .build();
form.addField(fieldKey);
// old (revoked) public key
if (mRevoked != null) {
String revokedKey = Base64.encodeToString(mRevoked.getEncoded(), Base64.NO_WRAP);
- FormField fieldRevoked = new FormField("revoked");
- fieldRevoked.setLabel("Revoked public key");
- fieldRevoked.setType(FormField.Type.text_single);
- fieldRevoked.addValue(revokedKey);
+ FormField fieldRevoked = FormField.builder("revoked")
+ .setLabel("Revoked public key")
+ .setType(FormField.Type.text_single)
+ .addValue(revokedKey)
+ .build();
form.addField(fieldRevoked);
}
@@ -173,7 +172,7 @@ protected void revokeCurrentKey()
public void processStanza(Stanza packet) {
IQ iq = (IQ) packet;
if (iq.getType() == IQ.Type.result) {
- DataForm response = iq.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(iq);
if (response != null) {
String publicKey = null;
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java
index d447ed312..c3823f73c 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java
@@ -32,7 +32,6 @@
import org.kontalk.Log;
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
-import org.kontalk.provider.MyUsers;
import org.kontalk.service.msgcenter.event.RosterLoadedEvent;
import org.kontalk.service.msgcenter.event.UserSubscribedEvent;
@@ -78,10 +77,10 @@ public void onRosterLoadingFailed(Exception exception) {
public void entriesAdded(Collection addresses) {
final MessageCenterService service = mService.get();
for (Jid jid : addresses) {
- if (Keyring.getPublicKey(service, jid.toString(), MyUsers.Keys.TRUST_UNKNOWN) == null) {
+ if (Keyring.getPublicKey(service, jid.toString(), Keyring.TRUST_UNKNOWN) == null) {
// autotrust the first key we have
// but set the trust level to ignored because we didn't really verify it
- Keyring.setAutoTrustLevel(service, jid.toString(), MyUsers.Keys.TRUST_IGNORED);
+ Keyring.setAutoTrustLevel(service, jid.toString(), Keyring.TRUST_IGNORED);
}
}
}
diff --git a/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java b/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java
index ef07e11bc..787ccf796 100644
--- a/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java
+++ b/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java
@@ -18,7 +18,7 @@
package org.kontalk.service.msgcenter.event;
-import org.jivesoftware.smack.packet.id.StanzaIdUtil;
+import org.jivesoftware.smack.packet.id.StandardStanzaIdSource;
import org.jxmpp.jid.Jid;
@@ -31,7 +31,7 @@ public class PublicKeyRequest extends RequestEvent {
public final Jid jid;
public PublicKeyRequest(Jid jid) {
- this(StanzaIdUtil.newStanzaId(), jid);
+ this(StandardStanzaIdSource.DEFAULT.getNewStanzaId(), jid);
}
/** Use null jid to request public keys for the whole roster. */
diff --git a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java
index a7f0d73a3..0e99c6c38 100644
--- a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java
+++ b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java
@@ -674,7 +674,7 @@ public void onVerificationRequest(VerificationRequest request) {
disconnect();
}
- DataForm response = result.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(result);
if (response != null && response.hasField("accept-terms")) {
FormField termsUrlField = response.getField("terms");
if (termsUrlField != null) {
@@ -748,6 +748,10 @@ public void onImportKeyRequest(ImportKeyRequest request) {
if (!TextUtils.isEmpty(phoneNumber)) {
cstate.phoneNumber = phoneNumber;
}
+ String displayName = accountInfo.get("displayName");
+ if (!TextUtils.isEmpty(displayName)) {
+ cstate.displayName = displayName;
+ }
}
// personal key corrupted or too old
@@ -761,7 +765,6 @@ public void onImportKeyRequest(ImportKeyRequest request) {
cstate.server = (request.server != null) ? request.server :
new EndpointServer(XmppStringUtils.parseDomain(uid.getEmail()));
- cstate.displayName = uid.getName();
cstate.passphrase = request.passphrase;
// copy over the parsed keys (imported keys may be armored)
cstate.privateKey = privateKeyBuf.toByteArray();
@@ -835,7 +838,7 @@ public void onImportKeyRequest(ImportKeyRequest request) {
disconnect();
}
- DataForm response = result.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(result);
if (response != null && response.hasField("accept-terms")) {
FormField termsUrlField = response.getField("terms");
if (termsUrlField != null) {
@@ -980,7 +983,7 @@ public void onChallengeRequest(ChallengeRequest request) {
// needed for connection
String userId = XMPPUtils.createLocalpart(cstate.phoneNumber);
keyRing = cstate.key.storeNetwork(userId, mConnector.getNetwork(),
- cstate.displayName, cstate.passphrase);
+ cstate.passphrase);
}
else {
keyRing = PersonalKey.test(cstate.privateKey, cstate.publicKey, cstate.passphrase, null);
@@ -1008,7 +1011,7 @@ public void onChallengeRequest(ChallengeRequest request) {
disconnect();
}
- DataForm response = result.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(result);
if (response != null) {
String publicKey = null;
@@ -1072,7 +1075,7 @@ private void requestRegistration() {
disconnect();
}
- DataForm response = result.getExtension("x", "jabber:x:data");
+ DataForm response = DataForm.from(result);
if (response != null) {
// ok! message will be sent
String smsFrom = null, challenge = null,
@@ -1210,7 +1213,6 @@ private void loadRetrievedKey() {
cstate.server = (cstate.server != null) ? cstate.server :
new EndpointServer(XmppStringUtils.parseDomain(uid.getEmail()));
- cstate.displayName = uid.getName();
// copy over the parsed keys (it should be the same, but you never know...)
cstate.privateKey = privateKeyBuf.toByteArray();
cstate.publicKey = publicKeyBuf.toByteArray();
@@ -1347,42 +1349,39 @@ private IQ createRegistrationForm(String phoneNumber, boolean acceptTerms, boole
iq.setTo(mConnector.getConnection().getXMPPServiceDomain());
Form form = new Form(DataForm.Type.submit);
- FormField type = new FormField("FORM_TYPE");
- type.setType(FormField.Type.hidden);
- type.addValue(Registration.NAMESPACE);
- form.addField(type);
+ form.addField(FormField.hiddenFormType(Registration.NAMESPACE));
- FormField phone = new FormField("phone");
- phone.setType(FormField.Type.text_single);
- phone.addValue(phoneNumber);
- form.addField(phone);
+ form.addField(FormField.builder("phone")
+ .setType(FormField.Type.text_single)
+ .addValue(phoneNumber)
+ .build());
if (acceptTerms) {
- FormField fAcceptTerms = new FormField("accept-terms");
- fAcceptTerms.setType(FormField.Type.bool);
- fAcceptTerms.addValue(Boolean.TRUE.toString());
- form.addField(fAcceptTerms);
+ form.addField(FormField.builder("accept-terms")
+ .setType(FormField.Type.bool)
+ .addValue(Boolean.TRUE.toString())
+ .build());
}
if (force) {
- FormField fForce = new FormField("force");
- fForce.setType(FormField.Type.bool);
- fForce.addValue(Boolean.TRUE.toString());
- form.addField(fForce);
+ form.addField(FormField.builder("force")
+ .setType(FormField.Type.bool)
+ .addValue(Boolean.TRUE.toString())
+ .build());
}
if (fallback) {
- FormField fFallback = new FormField("fallback");
- fFallback.setType(FormField.Type.bool);
- fFallback.addValue(Boolean.TRUE.toString());
- form.addField(fFallback);
+ form.addField(FormField.builder("fallback")
+ .setType(FormField.Type.bool)
+ .addValue(Boolean.TRUE.toString())
+ .build());
}
else {
// not falling back, ask for our preferred challenge
- FormField challenge = new FormField("challenge");
- challenge.setType(FormField.Type.text_single);
- challenge.addValue(DEFAULT_CHALLENGE);
- form.addField(challenge);
+ form.addField(FormField.builder("challenge")
+ .setType(FormField.Type.text_single)
+ .addValue(DEFAULT_CHALLENGE)
+ .build());
}
iq.addExtension(form.getDataFormToSend());
@@ -1395,17 +1394,14 @@ private IQ createChallengeForm(CharSequence code) {
iq.setTo(mConnector.getConnection().getXMPPServiceDomain());
Form form = new Form(DataForm.Type.submit);
- FormField type = new FormField("FORM_TYPE");
- type.setType(FormField.Type.hidden);
- type.addValue("http://kontalk.org/protocol/register#code");
- form.addField(type);
+ form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#code"));
if (code != null) {
- FormField codeField = new FormField("code");
- codeField.setLabel("Validation code");
- codeField.setType(FormField.Type.text_single);
- codeField.addValue(code.toString());
- form.addField(codeField);
+ form.addField(FormField.builder("code")
+ .setLabel("Validation code")
+ .setType(FormField.Type.text_single)
+ .addValue(code.toString())
+ .build());
}
iq.addExtension(form.getDataFormToSend());
diff --git a/app/src/main/java/org/kontalk/sync/Syncer.java b/app/src/main/java/org/kontalk/sync/Syncer.java
index ede057f3a..d4d36679b 100644
--- a/app/src/main/java/org/kontalk/sync/Syncer.java
+++ b/app/src/main/java/org/kontalk/sync/Syncer.java
@@ -53,7 +53,6 @@
import org.kontalk.crypto.PGPUserID;
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.MyUsers.Users;
import org.kontalk.service.msgcenter.MessageCenterService;
@@ -284,7 +283,7 @@ void performSync(Context context, Account account, String authority,
PGPPublicKey pubKey = PGP.getMasterKey(entry.publicKey);
// trust our own key blindly
int trustLevel = myAccount.isSelfJID(entry.from) ?
- MyUsers.Keys.TRUST_VERIFIED : -1;
+ Keyring.TRUST_VERIFIED : -1;
// update keys table immediately
Keyring.setKey(mContext, entry.from.toString(), entry.publicKey, trustLevel);
diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java
index b75538347..61d758129 100644
--- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java
+++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java
@@ -76,7 +76,6 @@
import org.kontalk.provider.MessagesProviderClient;
import org.kontalk.provider.MyMessages;
import org.kontalk.provider.MyMessages.Threads;
-import org.kontalk.provider.MyUsers;
import org.kontalk.provider.UsersProvider;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.service.msgcenter.PrivacyCommand;
@@ -527,7 +526,7 @@ else if ((trustedPublicKey == null && event.fingerprint == null) || event.type =
else {
// autotrust the key we are about to request
// but set the trust level to ignored because we didn't really verify it
- Keyring.setAutoTrustLevel(context, event.jid.toString(), MyUsers.Keys.TRUST_IGNORED);
+ Keyring.setAutoTrustLevel(context, event.jid.toString(), Keyring.TRUST_IGNORED);
requestPublicKey(event.jid);
}
}
@@ -787,7 +786,7 @@ void setPrivacy(@NonNull Context ctx, PrivacyCommand action) {
if (fingerprint != null) {
Kontalk.get().getMessagesController()
.setTrustLevelAndRetryMessages(mUserJID,
- fingerprint, MyUsers.Keys.TRUST_VERIFIED);
+ fingerprint, Keyring.TRUST_VERIFIED);
}
}
@@ -893,7 +892,7 @@ void showIdentityDialog(boolean informationOnly, int titleId) {
String fingerprint;
String uid;
- PGPPublicKeyRing publicKey = Keyring.getPublicKey(getActivity(), mUserJID, MyUsers.Keys.TRUST_UNKNOWN);
+ PGPPublicKeyRing publicKey = Keyring.getPublicKey(getActivity(), mUserJID, Keyring.TRUST_UNKNOWN);
if (publicKey != null) {
PGPPublicKey pk = PGP.getMasterKey(publicKey);
fingerprint = PGP.formatFingerprint(PGP.getFingerprint(pk));
@@ -970,7 +969,7 @@ void trustKeyChange(@NonNull Context context, String fingerprint) {
if (fingerprint == null)
fingerprint = getContact().getFingerprint();
Kontalk.get().getMessagesController()
- .setTrustLevelAndRetryMessages(mUserJID, fingerprint, MyUsers.Keys.TRUST_VERIFIED);
+ .setTrustLevelAndRetryMessages(mUserJID, fingerprint, Keyring.TRUST_VERIFIED);
// reload contact
invalidateContact();
}
diff --git a/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java b/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java
index 73ed76735..3655a0446 100644
--- a/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java
+++ b/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java
@@ -47,7 +47,7 @@
import org.kontalk.R;
import org.kontalk.crypto.PGP;
import org.kontalk.data.Contact;
-import org.kontalk.provider.MyUsers;
+import org.kontalk.provider.Keyring;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.service.msgcenter.event.ConnectedEvent;
import org.kontalk.service.msgcenter.event.LastActivityEvent;
@@ -139,17 +139,17 @@ else if (mContact.isKeyChanged()) {
mTrustStatus.setEnabled(true);
switch (mContact.getTrustedLevel()) {
- case MyUsers.Keys.TRUST_UNKNOWN:
+ case Keyring.TRUST_UNKNOWN:
resId = R.drawable.ic_trust_unknown;
textId = R.string.trust_unknown;
trustButtonsVisibility = View.VISIBLE;
break;
- case MyUsers.Keys.TRUST_IGNORED:
+ case Keyring.TRUST_IGNORED:
resId = R.drawable.ic_trust_ignored;
textId = R.string.trust_ignored;
trustButtonsVisibility = View.VISIBLE;
break;
- case MyUsers.Keys.TRUST_VERIFIED:
+ case Keyring.TRUST_VERIFIED:
resId = R.drawable.ic_trust_verified;
textId = R.string.trust_verified;
trustButtonsVisibility = View.GONE;
@@ -385,19 +385,19 @@ public void onClick(View view) {
view.findViewById(R.id.btn_ignore).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_IGNORED);
+ trustKey(mContact.getFingerprint(), Keyring.TRUST_IGNORED);
}
});
view.findViewById(R.id.btn_refuse).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_UNKNOWN);
+ trustKey(mContact.getFingerprint(), Keyring.TRUST_UNKNOWN);
}
});
view.findViewById(R.id.btn_accept).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_VERIFIED);
+ trustKey(mContact.getFingerprint(), Keyring.TRUST_VERIFIED);
}
});
diff --git a/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java b/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java
index 2c5f0e925..bf35de38d 100644
--- a/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java
+++ b/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java
@@ -72,7 +72,6 @@
import org.kontalk.provider.MessagesProviderClient;
import org.kontalk.provider.MyMessages;
import org.kontalk.provider.MyMessages.Groups;
-import org.kontalk.provider.MyUsers;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.service.msgcenter.event.RosterStatusEvent;
import org.kontalk.service.msgcenter.event.RosterStatusRequest;
@@ -145,7 +144,7 @@ private void loadConversation(long threadId) {
mMembersAdapter.clear();
for (String jid : members) {
Contact c = Contact.findByUserId(getContext(), jid);
- if (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN)
+ if (c.isKeyChanged() || c.getTrustedLevel() == Keyring.TRUST_UNKNOWN)
showIgnoreAll = true;
boolean owner = KontalkGroup.checkOwnership(mConversation.getGroupJid(), jid);
boolean isSelfJid = jid.equalsIgnoreCase(selfJid);
@@ -440,7 +439,7 @@ private void showIdentityDialog(Contact c, Boolean subscribed) {
int titleResId = R.string.title_identity;
String uid;
- PGPPublicKeyRing publicKey = Keyring.getPublicKey(getContext(), jid, MyUsers.Keys.TRUST_UNKNOWN);
+ PGPPublicKeyRing publicKey = Keyring.getPublicKey(getContext(), jid, Keyring.TRUST_UNKNOWN);
if (publicKey != null) {
PGPPublicKey pk = PGP.getMasterKey(publicKey);
String rawFingerprint = PGP.getFingerprint(pk);
@@ -491,14 +490,14 @@ else if (subscribed) {
int trustedLevel;
if (c.isKeyChanged()) {
// the key has changed and was not trusted yet
- trustedLevel = MyUsers.Keys.TRUST_UNKNOWN;
+ trustedLevel = Keyring.TRUST_UNKNOWN;
}
else {
trustedLevel = c.getTrustedLevel();
}
switch (trustedLevel) {
- case MyUsers.Keys.TRUST_IGNORED:
+ case Keyring.TRUST_IGNORED:
trustStringId = R.string.trust_ignored;
trustSpans = new CharacterStyle[] {
SystemUtils.getTypefaceSpan(Typeface.BOLD),
@@ -506,7 +505,7 @@ else if (subscribed) {
};
break;
- case MyUsers.Keys.TRUST_VERIFIED:
+ case Keyring.TRUST_VERIFIED:
trustStringId = R.string.trust_verified;
trustSpans = new CharacterStyle[] {
SystemUtils.getTypefaceSpan(Typeface.BOLD),
@@ -514,7 +513,7 @@ else if (subscribed) {
};
break;
- case MyUsers.Keys.TRUST_UNKNOWN:
+ case Keyring.TRUST_UNKNOWN:
default:
trustStringId = R.string.trust_unknown;
trustSpans = new CharacterStyle[] {
@@ -550,15 +549,15 @@ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which)
switch (which) {
case POSITIVE:
// trust the key
- trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_VERIFIED);
+ trustKey(jid, dialogFingerprint, Keyring.TRUST_VERIFIED);
break;
case NEUTRAL:
// ignore the key
- trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_IGNORED);
+ trustKey(jid, dialogFingerprint, Keyring.TRUST_IGNORED);
break;
case NEGATIVE:
// untrust the key
- trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_UNKNOWN);
+ trustKey(jid, dialogFingerprint, Keyring.TRUST_UNKNOWN);
break;
}
}
@@ -744,8 +743,8 @@ public void ignoreAll() {
for (GroupMember m : mMembers) {
Contact c = m.contact;
String fingerprint = c.getFingerprint();
- if (fingerprint != null && (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN)) {
- Keyring.setTrustLevel(mContext, c.getJID(), fingerprint, MyUsers.Keys.TRUST_IGNORED);
+ if (fingerprint != null && (c.isKeyChanged() || c.getTrustedLevel() == Keyring.TRUST_UNKNOWN)) {
+ Keyring.setTrustLevel(mContext, c.getJID(), fingerprint, Keyring.TRUST_IGNORED);
Contact.invalidate(c.getJID());
}
}
diff --git a/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java b/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java
index b6d13cb72..015c0af12 100644
--- a/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java
+++ b/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java
@@ -62,7 +62,6 @@
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MyMessages;
import org.kontalk.provider.MyMessages.Groups;
-import org.kontalk.provider.MyUsers;
import org.kontalk.service.msgcenter.event.NoPresenceEvent;
import org.kontalk.service.msgcenter.event.PresenceEvent;
import org.kontalk.service.msgcenter.event.PresenceRequest;
@@ -431,7 +430,7 @@ else if ((trustedPublicKey == null && event.fingerprint == null) || event.type =
else {
// autotrust the key we are about to request
// but set the trust level to ignored because we didn't really verify it
- Keyring.setAutoTrustLevel(context, event.jid.toString(), MyUsers.Keys.TRUST_IGNORED);
+ Keyring.setAutoTrustLevel(context, event.jid.toString(), Keyring.TRUST_IGNORED);
requestPublicKey(event.jid);
}
}
diff --git a/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java b/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java
index 48fa34e7e..5d8d760e5 100644
--- a/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java
+++ b/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java
@@ -20,7 +20,7 @@
import org.kontalk.R;
import org.kontalk.data.Contact;
-import org.kontalk.provider.MyUsers;
+import org.kontalk.provider.Keyring;
import org.kontalk.util.SystemUtils;
import android.annotation.SuppressLint;
@@ -138,13 +138,13 @@ else if (contact.isKeyChanged()) {
}
else {
switch (contact.getTrustedLevel()) {
- case MyUsers.Keys.TRUST_UNKNOWN:
+ case Keyring.TRUST_UNKNOWN:
resId = R.drawable.ic_trust_unknown;
break;
- case MyUsers.Keys.TRUST_IGNORED:
+ case Keyring.TRUST_IGNORED:
resId = R.drawable.ic_trust_ignored;
break;
- case MyUsers.Keys.TRUST_VERIFIED:
+ case Keyring.TRUST_VERIFIED:
resId = R.drawable.ic_trust_verified;
break;
default:
diff --git a/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java b/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java
index 77201a0e5..2597257f8 100644
--- a/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java
+++ b/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java
@@ -20,10 +20,9 @@
import java.lang.ref.WeakReference;
-import org.jivesoftware.smack.SmackException;
-import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
-import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.SuccessCallback;
import org.jxmpp.jid.BareJid;
import org.kontalk.client.HTTPFileUpload;
@@ -57,23 +56,16 @@ public boolean requiresCertificate() {
public void getPostUrl(String filename, long size, String mime, final UrlCallback callback) {
HTTPFileUpload.Request request = new HTTPFileUpload.Request(filename, size, mime);
request.setTo(mService);
- try {
- connection().sendIqWithResponseCallback(request, new StanzaListener() {
+ connection().sendIqRequestAsync(request)
+ .onSuccess(new SuccessCallback() {
@Override
- public void processStanza(Stanza packet) throws SmackException.NotConnectedException {
- if (packet instanceof HTTPFileUpload.Slot) {
- HTTPFileUpload.Slot slot = (HTTPFileUpload.Slot) packet;
+ public void onSuccess(IQ result) {
+ if (result instanceof HTTPFileUpload.Slot) {
+ HTTPFileUpload.Slot slot = (HTTPFileUpload.Slot) result;
callback.callback(slot.getPutUrl(), slot.getGetUrl());
}
}
});
- }
- catch (SmackException.NotConnectedException e) {
- // ignored
- }
- catch (InterruptedException e) {
- // ignored
- }
}
}
diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java
index ffae7c286..f536133c1 100644
--- a/app/src/main/java/org/kontalk/util/MessageUtils.java
+++ b/app/src/main/java/org/kontalk/util/MessageUtils.java
@@ -33,7 +33,9 @@
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
+import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.util.StringUtils;
+import org.jxmpp.jid.Jid;
import org.bouncycastle.openpgp.PGPException;
import android.content.ContentValues;
@@ -436,7 +438,12 @@ else if ((securityFlags & Coder.SECURITY_ERROR_PUBLIC_KEY_UNAVAILABLE) != 0) {
}
else {
- details.append(res.getString(R.string.security_status_good));
+ if ((securityFlags & Coder.SECURITY_BASIC) != 0) {
+ details.append(res.getString(R.string.security_status_good));
+ }
+ else if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) {
+ details.append(res.getString(R.string.security_status_strong));
+ }
}
details.setSpan(STYLE_BOLD, startPos, details.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -574,11 +581,12 @@ public static String messageId() {
return StringUtils.randomString(30);
}
- public static File encryptFile(Context context, InputStream in, String[] users)
- throws GeneralSecurityException, IOException, PGPException {
+ public static File encryptFile(Context context, InputStream in, Jid[] users)
+ throws GeneralSecurityException, IOException, PGPException, SmackException.NotConnectedException {
PersonalKey key = Kontalk.get().getPersonalKey();
EndpointServer server = Kontalk.get().getEndpointServer();
- Coder coder = Keyring.getEncryptCoder(context, server, key, users);
+ // TODO advanced coder not supported yet
+ Coder coder = Keyring.getEncryptCoder(context, Coder.SECURITY_BASIC, null, server, key, users);
// create a temporary file to store encrypted data
File temp = File.createTempFile("media", null, context.getCacheDir());
FileOutputStream out = new FileOutputStream(temp);
diff --git a/app/src/main/java/org/kontalk/util/XMPPUtils.java b/app/src/main/java/org/kontalk/util/XMPPUtils.java
index da74acb0d..7112993b2 100644
--- a/app/src/main/java/org/kontalk/util/XMPPUtils.java
+++ b/app/src/main/java/org/kontalk/util/XMPPUtils.java
@@ -18,7 +18,6 @@
package org.kontalk.util;
-import java.io.StringReader;
import java.util.Collection;
import java.util.Date;
@@ -32,9 +31,7 @@
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.util.XmppStringUtils;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
+import org.jivesoftware.smack.xml.XmlPullParser;
import android.graphics.Color;
import androidx.annotation.ColorInt;
@@ -50,31 +47,17 @@ public class XMPPUtils {
private XMPPUtils() {}
- private static XmlPullParserFactory _xmlFactory;
-
- private static XmlPullParser getPullParser(String data) throws XmlPullParserException {
- if (_xmlFactory == null) {
- _xmlFactory = XmlPullParserFactory.newInstance();
- _xmlFactory.setNamespaceAware(true);
- }
-
- XmlPullParser parser = _xmlFactory.newPullParser();
- parser.setInput(new StringReader(data));
-
- return parser;
- }
-
/** Parses a <xmpp>-wrapped message stanza. */
public static Message parseMessageStanza(String data) throws Exception {
- XmlPullParser parser = getPullParser(data);
+ XmlPullParser parser = XMPPParserUtils.getPullParser(data);
boolean done = false, in_xmpp = false;
Message msg = null;
while (!done) {
- int eventType = parser.next();
+ XmlPullParser.Event eventType = parser.next();
- if (eventType == XmlPullParser.START_TAG) {
+ if (eventType == XmlPullParser.Event.START_ELEMENT) {
if ("xmpp".equals(parser.getName()))
in_xmpp = true;
@@ -84,7 +67,7 @@ else if ("message".equals(parser.getName()) && in_xmpp) {
}
}
- else if (eventType == XmlPullParser.END_TAG) {
+ else if (eventType == XmlPullParser.Event.END_ELEMENT) {
if ("xmpp".equals(parser.getName()))
done = true;
diff --git a/app/src/main/res/raw/service.providers b/app/src/main/res/raw/service.providers
index 7554c4ed9..ded1b3952 100644
--- a/app/src/main/res/raw/service.providers
+++ b/app/src/main/res/raw/service.providers
@@ -272,4 +272,163 @@
org.kontalk.client.Account$Provider
+
+
+ pubsub
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.PubSubProvider
+
+
+
+ create
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider
+
+
+
+ items
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.ItemsProvider
+
+
+
+ item
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.ItemProvider
+
+
+
+ subscriptions
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider
+
+
+
+ subscription
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider
+
+
+
+ affiliations
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.AffiliationsProvider
+
+
+
+ affiliation
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.AffiliationProvider
+
+
+
+ options
+ http://jabber.org/protocol/pubsub
+ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider
+
+
+
+
+
+ affiliation
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.AffiliationProvider
+
+
+
+ pubsub
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.PubSubProvider
+
+
+
+ configure
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider
+
+
+
+ default
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider
+
+
+
+ subscriptions
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider
+
+
+
+ subscription
+ http://jabber.org/protocol/pubsub#owner
+ org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider
+
+
+
+
+ event
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.EventProvider
+
+
+
+ configuration
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.ConfigEventProvider
+
+
+
+ delete
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider
+
+
+
+ options
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider
+
+
+
+ items
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.ItemsProvider
+
+
+
+ item
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.ItemProvider
+
+
+
+ retract
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.RetractEventProvider
+
+
+
+ purge
+ http://jabber.org/protocol/pubsub#event
+ org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider
+
+
+
+
+ encrypted
+ eu.siacs.conversations.axolotl
+ org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider
+
+
+ list
+ eu.siacs.conversations.axolotl
+ org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider
+
+
+ bundle
+ eu.siacs.conversations.axolotl
+ org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c0685c685..ab40e147c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -428,6 +428,7 @@
Unable to write personal key to external storage.
Unable to export personal key.
+ strong
good
bad
diff --git a/build.gradle b/build.gradle
index fe50d8e59..b9975bb4b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,7 +44,7 @@ allprojects {
applicationId = 'org.kontalk'
versionCode = 463
versionName = '4.4.0-beta13'
- minSdkVersion = 16
+ minSdkVersion = 19
targetSdkVersion = 29
compileSdkVersion = 29
smackVersion = project(':client-common-java').smackVersion
diff --git a/client-common-java b/client-common-java
index 4fae0985f..74c9a74c5 160000
--- a/client-common-java
+++ b/client-common-java
@@ -1 +1 @@
-Subproject commit 4fae0985f53837ee409b4fd4d7f49290a8cbf6dd
+Subproject commit 74c9a74c599b99a307a9885b858cb736efbec346