diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index 0916d1e1a7d178bce439bfbc1b66334acc4f5c2e..4d39315244a607410df11496ec2ae8cab672a23a 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/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(); + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0cf6fbe248756554170a15b4ad7d37facf659ebe..b7935c3b5362027c51f4251a716df9c25a96d71a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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 { diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index fe70241f90faf1d4117b02d5c0a3b8819d024f3c..4b2c2957f62aaa1e1a2e6e814312a4bb7b136843 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/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 getEnabledAccounts(final XmppConnectionService service) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a3826626678d9a2061dfe33ce7acad9d686a9274..9a51f9e51550624e70e07c2e20232be3140513d1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/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 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")) { diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java index bff72a06b5f595aa0f3d184c0b3cabad96317c46..3af144105420ffbbd222dd5f8c4423b0aaaf07d5 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java @@ -21,4 +21,8 @@ public class Bind extends Extension { final var inline = getInline(); return inline == null ? Collections.emptyList() : inline.getExtensions(Feature.class); } + + public void setTag(final String tag) { + this.addExtension(new Tag(tag)); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java new file mode 100644 index 0000000000000000000000000000000000000000..5fac1d9e378ba68012ac299d8ac98713fba9f985 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java new file mode 100644 index 0000000000000000000000000000000000000000..2594f5874b09baaadd100486d8e3b492db949f41 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java new file mode 100644 index 0000000000000000000000000000000000000000..8685ed3ff63285ecba2e012f0294cf6016c41017 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..bb2a0c68cfae29d8fc02c2eed963b3fe82075b61 --- /dev/null +++ b/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)); + } +} diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index 8cf8fe20296d24b9763d8ee263f3796c9acef97c..d2af65f8346c4d0f70c5f4572d6330f08180a482 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/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 mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>()); private final Set 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) {