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> 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 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