use new xml api for bind 2

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/AppSettings.java                           | 24 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java        |  7 
src/main/java/eu/siacs/conversations/utils/AccountUtils.java                    | 12 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                   | 28 
src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java               |  4 
src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java                | 17 
src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java             | 17 
src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java           | 17 
src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java          | 25 
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java | 60 
10 files changed, 160 insertions(+), 51 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/AppSettings.java 🔗

@@ -10,6 +10,8 @@ import androidx.preference.PreferenceManager;
 
 import com.google.common.base.Strings;
 
+import java.security.SecureRandom;
+
 public class AppSettings {
 
     public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
@@ -45,6 +47,7 @@ public class AppSettings {
     public static final String LARGE_FONT = "large_font";
 
     private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+    private static final String INSTALLATION_ID = "im.conversations.android.install_id";
 
     private final Context context;
 
@@ -140,4 +143,25 @@ public class AppSettings {
     public boolean isRequireChannelBinding() {
         return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding);
     }
+
+    public synchronized long getInstallationId() {
+        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        final long existing = sharedPreferences.getLong(INSTALLATION_ID, 0);
+        if (existing != 0) {
+            return existing;
+        }
+        final var secureRandom = new SecureRandom();
+        final var installationId = secureRandom.nextLong();
+        sharedPreferences.edit().putLong(INSTALLATION_ID, installationId).apply();
+        return installationId;
+    }
+
+    public synchronized void resetInstallationId() {
+        final var secureRandom = new SecureRandom();
+        final var installationId = secureRandom.nextLong();
+        PreferenceManager.getDefaultSharedPreferences(context)
+                .edit()
+                .putLong(INSTALLATION_ID, installationId)
+                .apply();
+    }
 }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -229,6 +229,8 @@ public class XmppConnectionService extends Service {
     public DatabaseBackend databaseBackend;
     private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
     private long mLastActivity = 0;
+
+    private final AppSettings appSettings = new AppSettings(this);
     private final FileBackend fileBackend = new FileBackend(this);
     private MemorizingTrustManager mMemorizingTrustManager;
     private final NotificationService mNotificationService = new NotificationService(this);
@@ -483,6 +485,10 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public AppSettings getAppSettings() {
+        return this.appSettings;
+    }
+
     public FileBackend getFileBackend() {
         return this.fileBackend;
     }
@@ -4633,7 +4639,6 @@ public class XmppConnectionService extends Service {
 
     public void updateMemorizingTrustManager() {
         final MemorizingTrustManager trustManager;
-        final var appSettings = new AppSettings(this);
         if (appSettings.isTrustSystemCAStore()) {
             trustManager = new MemorizingTrustManager(getApplicationContext());
         } else {

src/main/java/eu/siacs/conversations/utils/AccountUtils.java 🔗

@@ -38,23 +38,27 @@ public class AccountUtils {
         return false;
     }
 
-    public static String publicDeviceId(final Account account) {
+    public static String publicDeviceId(final Account account, final long installationId) {
         final UUID uuid;
         try {
             uuid = UUID.fromString(account.getUuid());
         } catch (final IllegalArgumentException e) {
             return account.getUuid();
         }
+        return createUuid4(uuid.getMostSignificantBits(), installationId).toString();
+    }
+
+    public static UUID createUuid4(long mostSigBits, long leastSigBits) {
         final byte[] bytes =
                 Bytes.concat(
-                        Longs.toByteArray(uuid.getLeastSignificantBits()),
-                        Longs.toByteArray(uuid.getLeastSignificantBits()));
+                        Longs.toByteArray(mostSigBits),
+                        Longs.toByteArray(leastSigBits));
         bytes[6] &= 0x0f; /* clear version        */
         bytes[6] |= 0x40; /* set to version 4     */
         bytes[8] &= 0x3f; /* clear variant        */
         bytes[8] |= 0x80; /* set to IETF variant  */
         final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
-        return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString();
+        return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
     }
 
     public static List<String> getEnabledAccounts(final XmppConnectionService service) {

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -23,6 +23,7 @@ import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 
 import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
@@ -78,6 +79,7 @@ import im.conversations.android.xmpp.model.sasl.Response;
 import im.conversations.android.xmpp.model.sasl.Success;
 import im.conversations.android.xmpp.model.sasl2.Authenticate;
 import im.conversations.android.xmpp.model.sasl2.Authentication;
+import im.conversations.android.xmpp.model.sasl2.UserAgent;
 import im.conversations.android.xmpp.model.sm.Ack;
 import im.conversations.android.xmpp.model.sm.Enable;
 import im.conversations.android.xmpp.model.sm.Enabled;
@@ -195,7 +197,7 @@ public class XmppConnection implements Runnable {
     public XmppConnection(final Account account, final XmppConnectionService service) {
         this.account = account;
         this.mXmppConnectionService = service;
-        this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
+        this.appSettings = mXmppConnectionService.getAppSettings();
         this.presenceListener = new PresenceParser(service, account);
         this.unregisteredIqListener = new IqParser(service, account);
         this.messageListener = new MessageParser(service, account);
@@ -1664,15 +1666,15 @@ public class XmppConnection implements Runnable {
         if (!Strings.isNullOrEmpty(firstMessage)) {
             authenticate.addChild("initial-response").setContent(firstMessage);
         }
-        final Element userAgent = authenticate.addChild("user-agent");
-        userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
-        userAgent
-                .addChild("software")
-                .setContent(mXmppConnectionService.getString(R.string.app_name));
+        final var userAgent =
+                authenticate.addExtension(
+                        new UserAgent(
+                                AccountUtils.publicDeviceId(
+                                        account, appSettings.getInstallationId())));
+        userAgent.setSoftware(
+                String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
         if (!PhoneHelper.isEmulator()) {
-            userAgent
-                    .addChild("device")
-                    .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
+            userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
         }
         // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
         // (because we would rather just do a normal SM/resume)
@@ -1698,7 +1700,7 @@ public class XmppConnection implements Runnable {
     private Bind generateBindRequest(final Collection<String> bindFeatures) {
         Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
         final var bind = new Bind();
-        bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
+        bind.setTag(BuildConfig.APP_NAME);
         if (bindFeatures.contains(Namespace.CARBONS)) {
             bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable());
         }
@@ -2292,6 +2294,10 @@ public class XmppConnection implements Runnable {
             return;
         }
         if (streamError.hasChild("conflict")) {
+            final var loginInfo = this.loginInfo;
+            if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
+                this.appSettings.resetInstallationId();
+            }
             account.setResource(createNewResource());
             Log.d(
                     Config.LOGTAG,
@@ -2299,7 +2305,7 @@ public class XmppConnection implements Runnable {
                             + ": switching resource due to conflict ("
                             + account.getResource()
                             + ")");
-            throw new IOException();
+            throw new IOException("Closed stream due to resource conflict");
         } else if (streamError.hasChild("host-unknown")) {
             throw new StateChangingException(Account.State.HOST_UNKNOWN);
         } else if (streamError.hasChild("policy-violation")) {

src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Tag extends Extension {
+
+    public Tag() {
+        super(Tag.class);
+    }
+
+    public Tag(final String tag) {
+        this();
+        setContent(tag);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Device extends Extension {
+
+    public Device() {
+        super(Device.class);
+    }
+
+    public Device(final String device) {
+        this();
+        this.setContent(device);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Software extends Extension {
+
+    public Software() {
+        super(Software.class);
+    }
+
+    public Software(final String software) {
+        this();
+        this.setContent(software);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java 🔗

@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class UserAgent extends Extension {
+
+    public UserAgent() {
+        super(UserAgent.class);
+    }
+
+    public UserAgent(final String userAgentId) {
+        this();
+        this.setAttribute("id", userAgentId);
+    }
+
+    public void setSoftware(final String software) {
+        this.addExtension(new Software(software));
+    }
+
+    public void setDevice(final String device) {
+        this.addExtension(new Device(device));
+    }
+}

src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java 🔗

@@ -5,16 +5,36 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.preference.PreferenceManager;
 import android.util.Log;
 
 import com.google.common.collect.ImmutableMap;
 
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
+import eu.siacs.conversations.crypto.TrustManagers;
+import eu.siacs.conversations.crypto.sasl.Plain;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Entry;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.utils.SmsRetrieverWrapper;
+import eu.siacs.conversations.utils.TLSSocketFactory;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -38,7 +58,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.UUID;
 import java.util.WeakHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -52,26 +71,6 @@ import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.android.PhoneNumberContact;
-import eu.siacs.conversations.crypto.TrustManagers;
-import eu.siacs.conversations.crypto.sasl.Plain;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Entry;
-import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.utils.AccountUtils;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
-import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.SmsRetrieverWrapper;
-import eu.siacs.conversations.utils.TLSSocketFactory;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import io.michaelrocks.libphonenumber.android.Phonenumber;
-
 public class QuickConversationsService extends AbstractQuickConversationsService {
 
 
@@ -88,8 +87,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
 
     private static final String BASE_URL = "https://" + API_DOMAIN;
 
-    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
-
     private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
     private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 
@@ -308,16 +305,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
     }
 
     private String getInstallationId() {
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
-        String id = preferences.getString(INSTALLATION_ID, null);
-        if (id != null) {
-            return id;
-        } else {
-            id = UUID.randomUUID().toString();
-            preferences.edit().putString(INSTALLATION_ID, id).apply();
-            return id;
-        }
-
+        final var appSettings = service.getAppSettings();
+        final long installationId = appSettings.getInstallationId();
+        return AccountUtils.createUuid4(installationId, installationId).toString();
     }
 
     private int getApiErrorCode(final Exception e) {