M CHANGELOG.md => CHANGELOG.md +8 -0
@@ 1,5 1,13 @@
# Changelog
+### Version 2.11.0
+
+* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects
+* Implement Channel Binding
+* Add ability to switch from audio call to video call
+* Add ability to delete own avatar
+* Add notification for missed calls
+
### Version 2.10.10
* Minor bug fixes
M build.gradle => build.gradle +7 -6
@@ 6,7 6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.2.2'
+ classpath 'com.android.tools.build:gradle:7.3.1'
}
}
@@ 49,7 49,7 @@ dependencies {
implementation 'androidx.viewpager:viewpager:1.0.0'
- playstoreImplementation('com.google.firebase:firebase-messaging:23.0.7') {
+ playstoreImplementation('com.google.firebase:firebase-messaging:23.1.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ 59,11 59,11 @@ dependencies {
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
implementation 'org.sufficientlysecure:openpgp-api:10.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
- implementation 'androidx.appcompat:appcompat:1.5.0'
- implementation 'androidx.exifinterface:exifinterface:1.3.3'
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'androidx.exifinterface:exifinterface:1.3.5'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
- implementation 'com.google.android.material:material:1.4.0'
+ implementation 'com.google.android.material:material:1.7.0'
implementation "androidx.emoji2:emoji2:1.2.0"
freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0"
@@ 77,7 77,8 @@ dependencies {
implementation 'org.whispersystems:signal-protocol-android:2.6.2'
implementation 'com.makeramen:roundedimageview:2.3.0'
implementation "com.wefika:flowlayout:0.4.1"
- implementation 'com.otaliastudios:transcoder:0.10.4'
+ //noinspection GradleDependency
+ implementation 'com.otaliastudios:transcoder:0.9.1'
implementation 'org.jxmpp:jxmpp-jid:1.0.3'
implementation 'org.osmdroid:osmdroid-android:6.1.11'
A fastlane/metadata/android/en-US/changelogs/42041.txt => fastlane/metadata/android/en-US/changelogs/42041.txt +5 -0
@@ 0,0 1,5 @@
+* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects
+* Implement Channel Binding
+* Add ability to switch from audio call to video call
+* Add ability to delete own avatar
+* Add notification for missed calls
M gradle/wrapper/gradle-wrapper.properties => gradle/wrapper/gradle-wrapper.properties +2 -2
@@ 1,6 1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
+distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
M src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java => src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java +1 -1
@@ 100,7 100,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
account.setOption(Account.OPTION_MAGIC_CREATE, true);
account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername);
if (this.preAuth != null) {
- account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
+ account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
}
xmppConnectionService.createAccount(account);
}
M src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java => src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java +1 -1
@@ 100,7 100,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
account.setOption(Account.OPTION_MAGIC_CREATE, true);
account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername);
if (this.preAuth != null) {
- account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
+ account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
}
xmppConnectionService.createAccount(account);
}
A src/conversations/res/values-hr/strings.xml => src/conversations/res/values-hr/strings.xml +16 -0
@@ 0,0 1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="pick_a_server">Odaberite svog XMPP davatelja usluga.</string>
+ <string name="use_conversations.im">Koristite conversations.im</string>
+ <string name="create_new_account">Napravi novi račun</string>
+ <string name="do_you_have_an_account">Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune.</string>
+ <string name="server_select_text">XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations.</string>
+ <string name="magic_create_text_on_x">Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu.</string>
+ <string name="magic_create_text_fixed">Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu.</string>
+ <string name="your_server_invitation">Vaša pozivnica za poslužitelj</string>
+ <string name="improperly_formatted_provisioning">Neispravno formatiran kod za dodjelu</string>
+ <string name="tap_share_button_send_invite">Dodirnite gumb za dijeljenje kako biste svom kontaktu poslali pozivnicu na %1$s.</string>
+ <string name="if_contact_is_nearby_use_qr">Ako je vaš kontakt u blizini, također može skenirati kod u nastavku kako bi prihvatio vašu pozivnicu.</string>
+ <string name="easy_invite_share_text">Pridružite se %1$s i razgovarajte sa mnom: %2$s</string>
+ <string name="share_invite_with">Podijelite pozivnicu s...</string>
+</resources><
\ No newline at end of file
M src/conversations/res/values-zh-rCN/strings.xml => src/conversations/res/values-zh-rCN/strings.xml +6 -6
@@ 1,12 1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="pick_a_server">选择您的XMPP提供者</string>
- <string name="use_conversations.im">使用conversations.im</string>
+ <string name="pick_a_server">选择您的 XMPP 提供者</string>
+ <string name="use_conversations.im">使用 conversations.im</string>
<string name="create_new_account">创建新账户</string>
- <string name="do_you_have_an_account">您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。</string>
- <string name="server_select_text">XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im; 特别适合与“对话”配合使用的提供商。</string>
- <string name="magic_create_text_on_x">您已受邀参加%1$s。 我们将指导您完成创建帐户的过程。\n选择%1$s作为提供者后,您可以通过提供其他人的完整XMPP地址与其他提供者的用户进行交流。</string>
- <string name="magic_create_text_fixed">您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。</string>
+ <string name="do_you_have_an_account">您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。</string>
+ <string name="server_select_text">XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。</string>
+ <string name="magic_create_text_on_x">您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。</string>
+ <string name="magic_create_text_fixed">您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。</string>
<string name="your_server_invitation">你的服务器邀请</string>
<string name="improperly_formatted_provisioning">格式不正确的配置代码</string>
<string name="tap_share_button_send_invite">点击分享按钮向您的联系人发送加入 %1$s 的邀请。</string>
M src/conversations/res/values-zh-rTW/strings.xml => src/conversations/res/values-zh-rTW/strings.xml +9 -1
@@ 3,6 3,14 @@
<string name="pick_a_server">挑選您的 XMPP 提供者</string>
<string name="use_conversations.im">使用 conversations.im</string>
<string name="create_new_account">建立新帳戶</string>
+ <string name="do_you_have_an_account">您已經擁有一個 XMPP 賬戶嗎?如果您之前使用過其他 XMPP 客戶端或 Conversations 的話,那麼您已經擁有 XMPP 賬戶了。如果沒有賬戶的話,您可以現在建立一個。\n提示:有些電子郵件服務供應商也會提供 XMPP 賬戶。</string>
+ <string name="server_select_text">XMPP 是提供者無關的即時訊息網絡。 任何你選擇的 XMPP 伺服器都可在此客戶端上使用。\n不過,我們令它在 Coversations.im 中建立帳戶變得更方便; Conversations.im 是特別適合 Conversations 的提供者</string>
+ <string name="magic_create_text_on_x">你已受邀參加 %1$s 。 我們將指導你完成建立帳戶的過程。選擇 %1$s 作爲提供者後,你可以將你完整的 XMPP 地址交給使用其他提供者的用戶,你便能與他們交流。</string>
+ <string name="magic_create_text_fixed">您已被邀請參加 %1$s 。 我們已經爲你選擇了一個用戶名。 我們將指導你完成建立帳戶的過程。\n將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們交流。</string>
<string name="your_server_invitation">您的伺服器邀請</string>
- <string name="share_invite_with">分享邀請至…</string>
+ <string name="improperly_formatted_provisioning">配置代碼格式不正確</string>
+ <string name="tap_share_button_send_invite">輕觸分享按鍵向您的聯絡人發送加入 %1$s 的邀請。</string>
+ <string name="if_contact_is_nearby_use_qr">如果你的聯絡人就在附近,他們也可以掃描下面的代碼來接受你的邀請。</string>
+ <string name="easy_invite_share_text">加入 %1$s 和我聊天:%2$s</string>
+ <string name="share_invite_with">分享邀請到...</string>
</resources>=
\ No newline at end of file
M src/main/AndroidManifest.xml => src/main/AndroidManifest.xml +3 -0
@@ 64,6 64,9 @@
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="resource/folder" />
</intent>
+ <intent>
+ <action android:name="android.intent.action.VIEW" />
+ </intent>
</queries>
M src/main/java/eu/siacs/conversations/Config.java => src/main/java/eu/siacs/conversations/Config.java +8 -3
@@ 15,10 15,9 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
public final class Config {
private static final int UNENCRYPTED = 1;
private static final int OPENPGP = 2;
- private static final int OTR = 4;
private static final int OMEMO = 8;
- private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO;
+ private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OMEMO;
public static boolean supportUnencrypted() {
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
@@ 32,6 31,10 @@ public final class Config {
return (ENCRYPTION_MASK & OMEMO) != 0;
}
+ public static boolean omemoOnly() {
+ return !multipleEncryptionChoices() && supportOmemo();
+ }
+
public static boolean multipleEncryptionChoices() {
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
}
@@ 57,6 60,8 @@ public final class Config {
public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5;
+ public static final boolean QUICKSTART_ENABLED = true;
+
//Notification settings
public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;
public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false;
@@ 210,5 215,5 @@ public final class Config {
// How deep nested quotes should be displayed. '2' means one quote nested in another.
public static final int QUOTE_MAX_DEPTH = 7;
// How deep nested quotes should be created on quoting a message.
- public static final int QUOTING_MAX_DEPTH = 1;
+ public static final int QUOTING_MAX_DEPTH = 2;
}
M src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java => src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java +9 -1
@@ 34,6 34,9 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.SettingsActivity;
@@ 52,8 55,13 @@ public class OmemoSetting {
}
public static void load(final Context context, final SharedPreferences sharedPreferences) {
+ if (Config.omemoOnly()) {
+ always = true;
+ encryption = Message.ENCRYPTION_AXOLOTL;
+ return;
+ }
final String value = sharedPreferences.getString(SettingsActivity.OMEMO_SETTING, context.getResources().getString(R.string.omemo_setting_default));
- switch (value) {
+ switch (Strings.nullToEmpty(value)) {
case "always":
always = true;
encryption = Message.ENCRYPTION_AXOLOTL;
M src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java => src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +9 -7
@@ 1,5 1,7 @@
package eu.siacs.conversations.crypto.axolotl;
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
import android.os.Bundle;
import android.security.KeyChain;
import android.util.Log;
@@ 499,7 501,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
Signature verifier = Signature.getInstance("sha256WithRSA");
- verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG());
+ verifier.initSign(x509PrivateKey, SECURE_RANDOM);
verifier.update(axolotlPublicKey.serialize());
byte[] signature = verifier.sign();
IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
@@ 708,11 710,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public void deleteOmemoIdentity() {
- final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
- final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node);
- mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null);
+ mXmppConnectionService.deletePepNode(
+ account, AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId());
final Set<Integer> ownDeviceIds = getOwnDeviceIds();
- publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds);
+ publishDeviceIdsAndRefineAccessModel(
+ ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds);
}
public List<Jid> getCryptoTargets(Conversation conversation) {
@@ 1270,7 1272,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
+ new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
);
}
return Futures.immediateFuture(
@@ 1304,7 1306,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload)
+ new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
);
}
processPostponed();
M src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java => src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +4 -5
@@ 1,16 1,15 @@
package eu.siacs.conversations.crypto.sasl;
-import java.security.SecureRandom;
+import javax.net.ssl.SSLSocket;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class Anonymous extends SaslMechanism {
public static final String MECHANISM = "ANONYMOUS";
- public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
- super(tagWriter, account, rng);
+ public Anonymous(final Account account) {
+ super(account);
}
@Override
@@ 24,7 23,7 @@ public class Anonymous extends SaslMechanism {
}
@Override
- public String getClientFirstMessage() {
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
return "";
}
}
A src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java => src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +120 -0
@@ 0,0 1,120 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Log;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableBiMap;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.SSLSockets;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+
+public enum ChannelBinding {
+ NONE,
+ TLS_EXPORTER,
+ TLS_SERVER_END_POINT,
+ TLS_UNIQUE;
+
+ public static final BiMap<ChannelBinding, String> SHORT_NAMES;
+
+ static {
+ final ImmutableBiMap.Builder<ChannelBinding, String> builder = ImmutableBiMap.builder();
+ for (final ChannelBinding cb : values()) {
+ builder.put(cb, shortName(cb));
+ }
+ SHORT_NAMES = builder.build();
+ }
+
+ public static Collection<ChannelBinding> of(final Element channelBinding) {
+ Preconditions.checkArgument(
+ channelBinding == null
+ || ("sasl-channel-binding".equals(channelBinding.getName())
+ && Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())),
+ "pass null or a valid channel binding stream feature");
+ return Collections2.filter(
+ Collections2.transform(
+ Collections2.filter(
+ channelBinding == null
+ ? Collections.emptyList()
+ : channelBinding.getChildren(),
+ c -> c != null && "channel-binding".equals(c.getName())),
+ c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))),
+ Predicates.notNull());
+ }
+
+ private static ChannelBinding of(final String type) {
+ if (type == null) {
+ return null;
+ }
+ try {
+ return valueOf(
+ CaseFormat.LOWER_HYPHEN.converterTo(CaseFormat.UPPER_UNDERSCORE).convert(type));
+ } catch (final IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, type + " is not a known channel binding");
+ return null;
+ }
+ }
+
+ public static ChannelBinding get(final String name) {
+ if (Strings.isNullOrEmpty(name)) {
+ return NONE;
+ }
+ try {
+ return valueOf(name);
+ } catch (final IllegalArgumentException e) {
+ return NONE;
+ }
+ }
+
+ public static ChannelBinding best(
+ final Collection<ChannelBinding> bindings, final SSLSockets.Version sslVersion) {
+ if (sslVersion == SSLSockets.Version.NONE) {
+ return NONE;
+ }
+ if (bindings.contains(TLS_EXPORTER) && sslVersion == SSLSockets.Version.TLS_1_3) {
+ return TLS_EXPORTER;
+ } else if (bindings.contains(TLS_UNIQUE)
+ && Arrays.asList(
+ SSLSockets.Version.TLS_1_0,
+ SSLSockets.Version.TLS_1_1,
+ SSLSockets.Version.TLS_1_2)
+ .contains(sslVersion)) {
+ return TLS_UNIQUE;
+ } else if (bindings.contains(TLS_SERVER_END_POINT)) {
+ return TLS_SERVER_END_POINT;
+ } else {
+ return NONE;
+ }
+ }
+
+ public static boolean isAvailable(
+ final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) {
+ return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion)
+ == channelBinding;
+ }
+
+ private static String shortName(final ChannelBinding channelBinding) {
+ switch (channelBinding) {
+ case TLS_UNIQUE:
+ return "UNIQ";
+ case TLS_EXPORTER:
+ return "EXPR";
+ case TLS_SERVER_END_POINT:
+ return "ENDP";
+ case NONE:
+ return "NONE";
+ default:
+ throw new AssertionError("Missing short name for " + channelBinding);
+ }
+ }
+}
A src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java => src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +100 -0
@@ 0,0 1,100 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import org.bouncycastle.jcajce.provider.digest.SHA256;
+import org.conscrypt.Conscrypt;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+public interface ChannelBindingMechanism {
+
+ String EXPORTER_LABEL = "EXPORTER-Channel-Binding";
+
+ ChannelBinding getChannelBinding();
+
+ static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding)
+ throws SaslMechanism.AuthenticationException {
+ if (sslSocket == null) {
+ throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket");
+ }
+ if (channelBinding == ChannelBinding.TLS_EXPORTER) {
+ final byte[] keyingMaterial;
+ try {
+ keyingMaterial =
+ Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32);
+ } catch (final SSLException e) {
+ throw new SaslMechanism.AuthenticationException("Could not export keying material");
+ }
+ if (keyingMaterial == null) {
+ throw new SaslMechanism.AuthenticationException(
+ "Could not export keying material. Socket not ready");
+ }
+ return keyingMaterial;
+ } else if (channelBinding == ChannelBinding.TLS_UNIQUE) {
+ final byte[] unique = Conscrypt.getTlsUnique(sslSocket);
+ if (unique == null) {
+ throw new SaslMechanism.AuthenticationException(
+ "Could not retrieve tls unique. Socket not ready");
+ }
+ return unique;
+ } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
+ return getServerEndPointChannelBinding(sslSocket.getSession());
+ } else {
+ throw new SaslMechanism.AuthenticationException(
+ String.format("%s is not a valid channel binding", channelBinding));
+ }
+ }
+
+ static byte[] getServerEndPointChannelBinding(final SSLSession session)
+ throws SaslMechanism.AuthenticationException {
+ final Certificate[] certificates;
+ try {
+ certificates = session.getPeerCertificates();
+ } catch (final SSLPeerUnverifiedException e) {
+ throw new SaslMechanism.AuthenticationException("Could not verify peer certificates");
+ }
+ if (certificates == null || certificates.length == 0) {
+ throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate");
+ }
+ final X509Certificate certificate;
+ if (certificates[0] instanceof X509Certificate) {
+ certificate = (X509Certificate) certificates[0];
+ } else {
+ throw new SaslMechanism.AuthenticationException("Certificate was not X509");
+ }
+ final String algorithm = certificate.getSigAlgName();
+ final int withIndex = algorithm.indexOf("with");
+ if (withIndex <= 0) {
+ throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName");
+ }
+ final String hashAlgorithm = algorithm.substring(0, withIndex);
+ final MessageDigest messageDigest;
+ // https://www.rfc-editor.org/rfc/rfc5929#section-4.1
+ if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) {
+ messageDigest = new SHA256.Digest();
+ } else {
+ try {
+ messageDigest = MessageDigest.getInstance(hashAlgorithm);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new SaslMechanism.AuthenticationException(
+ "Could not instantiate message digest for " + hashAlgorithm);
+ }
+ }
+ final byte[] encodedCertificate;
+ try {
+ encodedCertificate = certificate.getEncoded();
+ } catch (final CertificateEncodingException e) {
+ throw new SaslMechanism.AuthenticationException("Could not encode certificate");
+ }
+ messageDigest.update(encodedCertificate);
+ return messageDigest.digest();
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java => src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +49 -28
@@ 5,18 5,19 @@ import android.util.Base64;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
+
+import javax.net.ssl.SSLSocket;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.xml.TagWriter;
public class DigestMd5 extends SaslMechanism {
public static final String MECHANISM = "DIGEST-MD5";
+ private State state = State.INITIAL;
- public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- super(tagWriter, account, rng);
+ public DigestMd5(final Account account) {
+ super(account);
}
@Override
@@ 29,16 30,16 @@ public class DigestMd5 extends SaslMechanism {
return MECHANISM;
}
- private State state = State.INITIAL;
-
@Override
- public String getResponse(final String challenge) throws AuthenticationException {
+ public String getResponse(final String challenge, final SSLSocket sslSocket)
+ throws AuthenticationException {
switch (state) {
case INITIAL:
state = State.RESPONSE_SENT;
final String encodedResponse;
try {
- final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
+ final Tokenizer tokenizer =
+ new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
String nonce = "";
for (final String token : tokenizer) {
final String[] parts = token.split("=", 2);
@@ 50,29 51,49 @@ public class DigestMd5 extends SaslMechanism {
}
final String digestUri = "xmpp/" + account.getServer();
final String nonceCount = "00000001";
- final String x = account.getUsername() + ":" + account.getServer() + ":"
- + account.getPassword();
+ final String x =
+ account.getUsername()
+ + ":"
+ + account.getServer()
+ + ":"
+ + account.getPassword();
final MessageDigest md = MessageDigest.getInstance("MD5");
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
- final String cNonce = CryptoHelper.random(100, rng);
- final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
- (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
+ final String cNonce = CryptoHelper.random(100);
+ final byte[] a1 =
+ CryptoHelper.concatenateByteArrays(
+ y,
+ (":" + nonce + ":" + cNonce)
+ .getBytes(Charset.defaultCharset()));
final String a2 = "AUTHENTICATE:" + digestUri;
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
- final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
- .defaultCharset())));
- final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
- + ":auth:" + ha2;
- final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
- .defaultCharset())));
- final String saslString = "username=\"" + account.getUsername()
- + "\",realm=\"" + account.getServer() + "\",nonce=\""
- + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
- + ",qop=auth,digest-uri=\"" + digestUri + "\",response="
- + response + ",charset=utf-8";
- encodedResponse = Base64.encodeToString(
- saslString.getBytes(Charset.defaultCharset()),
- Base64.NO_WRAP);
+ final String ha2 =
+ CryptoHelper.bytesToHex(
+ md.digest(a2.getBytes(Charset.defaultCharset())));
+ final String kd =
+ ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2;
+ final String response =
+ CryptoHelper.bytesToHex(
+ md.digest(kd.getBytes(Charset.defaultCharset())));
+ final String saslString =
+ "username=\""
+ + account.getUsername()
+ + "\",realm=\""
+ + account.getServer()
+ + "\",nonce=\""
+ + nonce
+ + "\",cnonce=\""
+ + cNonce
+ + "\",nc="
+ + nonceCount
+ + ",qop=auth,digest-uri=\""
+ + digestUri
+ + "\",response="
+ + response
+ + ",charset=utf-8";
+ encodedResponse =
+ Base64.encodeToString(
+ saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
} catch (final NoSuchAlgorithmException e) {
throw new AuthenticationException(e);
}
@@ 83,7 104,7 @@ public class DigestMd5 extends SaslMechanism {
break;
case VALID_SERVER_RESPONSE:
if (challenge == null) {
- return null; //everything is fine
+ return null; // everything is fine
}
default:
throw new InvalidStateException(state);
M src/main/java/eu/siacs/conversations/crypto/sasl/External.java => src/main/java/eu/siacs/conversations/crypto/sasl/External.java +6 -6
@@ 2,17 2,16 @@ package eu.siacs.conversations.crypto.sasl;
import android.util.Base64;
-import java.security.SecureRandom;
+import javax.net.ssl.SSLSocket;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class External extends SaslMechanism {
public static final String MECHANISM = "EXTERNAL";
- public External(TagWriter tagWriter, Account account, SecureRandom rng) {
- super(tagWriter, account, rng);
+ public External(final Account account) {
+ super(account);
}
@Override
@@ 26,7 25,8 @@ public class External extends SaslMechanism {
}
@Override
- public String getClientFirstMessage() {
- return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
+ return Base64.encodeToString(
+ account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
}
}
A src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java => src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +190 -0
@@ 0,0 1,190 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.hash.HashFunction;
+import com.google.common.primitives.Bytes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.SSLSocket;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.SSLSockets;
+
+public abstract class HashedToken extends SaslMechanism implements ChannelBindingMechanism {
+
+ private static final String PREFIX = "HT";
+
+ private static final List<String> HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256");
+ private static final byte[] INITIATOR = "Initiator".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RESPONDER = "Responder".getBytes(StandardCharsets.UTF_8);
+
+ protected final ChannelBinding channelBinding;
+
+ protected HashedToken(final Account account, final ChannelBinding channelBinding) {
+ super(account);
+ this.channelBinding = channelBinding;
+ }
+
+ @Override
+ public int getPriority() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
+ final String token = Strings.nullToEmpty(this.account.getFastToken());
+ final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
+ final byte[] cbData = getChannelBindingData(sslSocket);
+ final byte[] initiatorHashedToken =
+ hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes();
+ final byte[] firstMessage =
+ Bytes.concat(
+ account.getUsername().getBytes(StandardCharsets.UTF_8),
+ new byte[] {0x00},
+ initiatorHashedToken);
+ return Base64.encodeToString(firstMessage, Base64.NO_WRAP);
+ }
+
+ private byte[] getChannelBindingData(final SSLSocket sslSocket) {
+ if (this.channelBinding == ChannelBinding.NONE) {
+ return new byte[0];
+ }
+ try {
+ return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
+ } catch (final AuthenticationException e) {
+ Log.e(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": unable to retrieve channel binding data for "
+ + getMechanism(),
+ e);
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public String getResponse(final String challenge, final SSLSocket socket)
+ throws AuthenticationException {
+ final byte[] responderMessage;
+ try {
+ responderMessage = Base64.decode(challenge, Base64.NO_WRAP);
+ } catch (final Exception e) {
+ throw new AuthenticationException("Unable to decode responder message", e);
+ }
+ final String token = Strings.nullToEmpty(this.account.getFastToken());
+ final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
+ final byte[] cbData = getChannelBindingData(socket);
+ final byte[] expectedResponderMessage =
+ hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes();
+ if (Arrays.equals(responderMessage, expectedResponderMessage)) {
+ return null;
+ }
+ throw new AuthenticationException("Responder message did not match");
+ }
+
+ protected abstract HashFunction getHashFunction(final byte[] key);
+
+ public abstract Mechanism getTokenMechanism();
+
+ @Override
+ public String getMechanism() {
+ return getTokenMechanism().name();
+ }
+
+ public static final class Mechanism {
+ public final String hashFunction;
+ public final ChannelBinding channelBinding;
+
+ public Mechanism(String hashFunction, ChannelBinding channelBinding) {
+ this.hashFunction = hashFunction;
+ this.channelBinding = channelBinding;
+ }
+
+ public static Mechanism of(final String mechanism) {
+ final int first = mechanism.indexOf('-');
+ final int last = mechanism.lastIndexOf('-');
+ if (last <= first || mechanism.length() <= last) {
+ throw new IllegalArgumentException("Not a valid HashedToken name");
+ }
+ if (mechanism.substring(0, first).equals(PREFIX)) {
+ final String hashFunction = mechanism.substring(first + 1, last);
+ final String cbShortName = mechanism.substring(last + 1);
+ final ChannelBinding channelBinding =
+ ChannelBinding.SHORT_NAMES.inverse().get(cbShortName);
+ if (channelBinding == null) {
+ throw new IllegalArgumentException("Unknown channel binding " + cbShortName);
+ }
+ return new Mechanism(hashFunction, channelBinding);
+ } else {
+ throw new IllegalArgumentException("HashedToken name does not start with HT");
+ }
+ }
+
+ public static Mechanism ofOrNull(final String mechanism) {
+ try {
+ return mechanism == null ? null : of(mechanism);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ public static Multimap<String, ChannelBinding> of(final Collection<String> mechanisms) {
+ final ImmutableMultimap.Builder<String, ChannelBinding> builder =
+ ImmutableMultimap.builder();
+ for (final String name : mechanisms) {
+ try {
+ final Mechanism mechanism = Mechanism.of(name);
+ builder.put(mechanism.hashFunction, mechanism.channelBinding);
+ } catch (final IllegalArgumentException ignored) {
+ }
+ }
+ return builder.build();
+ }
+
+ public static Mechanism best(
+ final Collection<String> mechanisms, final SSLSockets.Version sslVersion) {
+ final Multimap<String, ChannelBinding> multimap = of(mechanisms);
+ for (final String hashFunction : HASH_FUNCTIONS) {
+ final Collection<ChannelBinding> channelBindings = multimap.get(hashFunction);
+ if (channelBindings.isEmpty()) {
+ continue;
+ }
+ final ChannelBinding cb = ChannelBinding.best(channelBindings, sslVersion);
+ return new Mechanism(hashFunction, cb);
+ }
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("hashFunction", hashFunction)
+ .add("channelBinding", channelBinding)
+ .toString();
+ }
+
+ public String name() {
+ return String.format(
+ "%s-%s-%s",
+ PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding));
+ }
+ }
+
+ public ChannelBinding getChannelBinding() {
+ return this.channelBinding;
+ }
+}
A src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java => src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java +23 -0
@@ 0,0 1,23 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class HashedTokenSha256 extends HashedToken {
+
+ public HashedTokenSha256(final Account account, final ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected HashFunction getHashFunction(final byte[] key) {
+ return Hashing.hmacSha256(key);
+ }
+
+ @Override
+ public Mechanism getTokenMechanism() {
+ return new Mechanism("SHA-256", channelBinding);
+ }
+}
A src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java => src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java +23 -0
@@ 0,0 1,23 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class HashedTokenSha512 extends HashedToken {
+
+ public HashedTokenSha512(final Account account, final ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected HashFunction getHashFunction(final byte[] key) {
+ return Hashing.hmacSha512(key);
+ }
+
+ @Override
+ public Mechanism getTokenMechanism() {
+ return new Mechanism("SHA-512", this.channelBinding);
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java => src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +10 -9
@@ 4,15 4,21 @@ import android.util.Base64;
import java.nio.charset.Charset;
+import javax.net.ssl.SSLSocket;
+
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class Plain extends SaslMechanism {
public static final String MECHANISM = "PLAIN";
- public Plain(final TagWriter tagWriter, final Account account) {
- super(tagWriter, account, null);
+ public Plain(final Account account) {
+ super(account);
+ }
+
+ public static String getMessage(String username, String password) {
+ final String message = '\u0000' + username + '\u0000' + password;
+ return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
}
@Override
@@ 26,12 32,7 @@ public class Plain extends SaslMechanism {
}
@Override
- public String getClientFirstMessage() {
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
return getMessage(account.getUsername(), account.getPassword());
}
-
- public static String getMessage(String username, String password) {
- final String message = '\u0000' + username + '\u0000' + password;
- return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
- }
}
M src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java => src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +150 -23
@@ 1,15 1,68 @@
package eu.siacs.conversations.crypto.sasl;
-import java.security.SecureRandom;
+import android.util.Log;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.net.ssl.SSLSocket;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
+import eu.siacs.conversations.utils.SSLSockets;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
public abstract class SaslMechanism {
- final protected TagWriter tagWriter;
- final protected Account account;
- final protected SecureRandom rng;
+ protected final Account account;
+
+ protected SaslMechanism(final Account account) {
+ this.account = account;
+ }
+
+ public static String namespace(final Version version) {
+ if (version == Version.SASL) {
+ return Namespace.SASL;
+ } else {
+ return Namespace.SASL_2;
+ }
+ }
+
+ /**
+ * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be
+ * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism
+ * of lower priority (to prevent downgrade attacks).
+ *
+ * @return An arbitrary int representing the priority
+ */
+ public abstract int getPriority();
+
+ public abstract String getMechanism();
+
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
+ return "";
+ }
+
+ public String getResponse(final String challenge, final SSLSocket sslSocket)
+ throws AuthenticationException {
+ return "";
+ }
+
+ public static Collection<String> mechanisms(final Element authElement) {
+ if (authElement == null) {
+ return Collections.emptyList();
+ }
+ return Collections2.transform(
+ Collections2.filter(
+ authElement.getChildren(),
+ c -> c != null && "mechanism".equals(c.getName())),
+ c -> c == null ? null : c.getContent());
+ }
protected enum State {
INITIAL,
@@ 18,6 71,22 @@ public abstract class SaslMechanism {
VALID_SERVER_RESPONSE,
}
+ public enum Version {
+ SASL,
+ SASL_2;
+
+ public static Version of(final Element element) {
+ switch (Strings.nullToEmpty(element.getNamespace())) {
+ case Namespace.SASL:
+ return SASL;
+ case Namespace.SASL_2:
+ return SASL_2;
+ default:
+ throw new IllegalArgumentException("Unrecognized SASL namespace");
+ }
+ }
+ }
+
public static class AuthenticationException extends Exception {
public AuthenticationException(final String message) {
super(message);
@@ 42,28 111,86 @@ public abstract class SaslMechanism {
}
}
- public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- this.tagWriter = tagWriter;
- this.account = account;
- this.rng = rng;
- }
+ public static final class Factory {
- /**
- * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
- * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
- * attacks).
- *
- * @return An arbitrary int representing the priority
- */
- public abstract int getPriority();
+ private final Account account;
- public abstract String getMechanism();
+ public Factory(final Account account) {
+ this.account = account;
+ }
- public String getClientFirstMessage() {
- return "";
+ private SaslMechanism of(
+ final Collection<String> mechanisms, final ChannelBinding channelBinding) {
+ Preconditions.checkNotNull(channelBinding, "Use ChannelBinding.NONE instead of null");
+ if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
+ return new External(account);
+ } else if (mechanisms.contains(ScramSha512Plus.MECHANISM)
+ && channelBinding != ChannelBinding.NONE) {
+ return new ScramSha512Plus(account, channelBinding);
+ } else if (mechanisms.contains(ScramSha256Plus.MECHANISM)
+ && channelBinding != ChannelBinding.NONE) {
+ return new ScramSha256Plus(account, channelBinding);
+ } else if (mechanisms.contains(ScramSha1Plus.MECHANISM)
+ && channelBinding != ChannelBinding.NONE) {
+ return new ScramSha1Plus(account, channelBinding);
+ } else if (mechanisms.contains(ScramSha512.MECHANISM)) {
+ return new ScramSha512(account);
+ } else if (mechanisms.contains(ScramSha256.MECHANISM)) {
+ return new ScramSha256(account);
+ } else if (mechanisms.contains(ScramSha1.MECHANISM)) {
+ return new ScramSha1(account);
+ } else if (mechanisms.contains(Plain.MECHANISM)
+ && !account.getServer().equals("nimbuzz.com")) {
+ return new Plain(account);
+ } else if (mechanisms.contains(DigestMd5.MECHANISM)) {
+ return new DigestMd5(account);
+ } else if (mechanisms.contains(Anonymous.MECHANISM)) {
+ return new Anonymous(account);
+ } else {
+ return null;
+ }
+ }
+
+ public SaslMechanism of(
+ final Collection<String> mechanisms,
+ final Collection<ChannelBinding> bindings,
+ final Version version,
+ final SSLSockets.Version sslVersion) {
+ final HashedToken fastMechanism = account.getFastMechanism();
+ if (version == Version.SASL_2 && fastMechanism != null) {
+ return fastMechanism;
+ }
+ final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion);
+ return of(mechanisms, channelBinding);
+ }
+
+ public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) {
+ return of(Collections.singleton(mechanism), channelBinding);
+ }
}
- public String getResponse(final String challenge) throws AuthenticationException {
- return "";
+ public static SaslMechanism ensureAvailable(
+ final SaslMechanism mechanism, final SSLSockets.Version sslVersion) {
+ if (mechanism instanceof ChannelBindingMechanism) {
+ final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding();
+ if (ChannelBinding.isAvailable(cb, sslVersion)) {
+ return mechanism;
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ "pinned channel binding method " + cb + " no longer available");
+ return null;
+ }
+ } else {
+ return mechanism;
+ }
+ }
+
+ public static boolean hashedToken(final SaslMechanism saslMechanism) {
+ return saslMechanism instanceof HashedToken;
+ }
+
+ public static boolean pin(final SaslMechanism saslMechanism) {
+ return !hashedToken(saslMechanism);
}
}
M src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +138 -92
@@ 1,105 1,85 @@
package eu.siacs.conversations.crypto.sasl;
import android.util.Base64;
+import android.util.Log;
+import com.google.common.base.CaseFormat;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-
-import org.bouncycastle.crypto.Digest;
-import org.bouncycastle.crypto.macs.HMac;
-import org.bouncycastle.crypto.params.KeyParameter;
+import com.google.common.hash.HashFunction;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
-import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
+import javax.net.ssl.SSLSocket;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.xml.TagWriter;
abstract class ScramMechanism extends SaslMechanism {
- // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
- private final static String GS2_HEADER = "n,,";
+
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
-
- protected abstract HMac getHMAC();
-
- protected abstract Digest getDigest();
-
- private static final Cache<CacheKey, KeyPair> CACHE = CacheBuilder.newBuilder().maximumSize(10).build();
-
- private static class CacheKey {
- final String algorithm;
- final String password;
- final String salt;
- final int iterations;
-
- private CacheKey(String algorithm, String password, String salt, int iterations) {
- this.algorithm = algorithm;
- this.password = password;
- this.salt = salt;
- this.iterations = iterations;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CacheKey cacheKey = (CacheKey) o;
- return iterations == cacheKey.iterations &&
- Objects.equal(algorithm, cacheKey.algorithm) &&
- Objects.equal(password, cacheKey.password) &&
- Objects.equal(salt, cacheKey.salt);
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(algorithm, password, salt, iterations);
- }
- }
-
- private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException {
- return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> {
- final byte[] saltedPassword, serverKey, clientKey;
- saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations);
- serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
- clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
- return new KeyPair(clientKey, serverKey);
- });
- }
-
+ private static final Cache<CacheKey, KeyPair> CACHE =
+ CacheBuilder.newBuilder().maximumSize(10).build();
+ protected final ChannelBinding channelBinding;
+ private final String gs2Header;
private final String clientNonce;
protected State state = State.INITIAL;
private String clientFirstMessageBare;
private byte[] serverSignature = null;
- ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- super(tagWriter, account, rng);
-
+ ScramMechanism(final Account account, final ChannelBinding channelBinding) {
+ super(account);
+ this.channelBinding = channelBinding;
+ if (channelBinding == ChannelBinding.NONE) {
+ // TODO this needs to be changed to "y,," for the scram internal down grade protection
+ // but we might risk compatibility issues if the server supports a binding that we don’t
+ // support
+ this.gs2Header = "n,,";
+ } else {
+ this.gs2Header =
+ String.format(
+ "p=%s,,",
+ CaseFormat.UPPER_UNDERSCORE
+ .converterTo(CaseFormat.LOWER_HYPHEN)
+ .convert(channelBinding.toString()));
+ }
// This nonce should be different for each authentication attempt.
- clientNonce = CryptoHelper.random(100, rng);
+ this.clientNonce = CryptoHelper.random(100);
clientFirstMessageBare = "";
}
+ protected abstract HashFunction getHMac(final byte[] key);
+
+ protected abstract HashFunction getDigest();
+
+ private KeyPair getKeyPair(final String password, final String salt, final int iterations)
+ throws ExecutionException {
+ return CACHE.get(
+ new CacheKey(getMechanism(), password, salt, iterations),
+ () -> {
+ final byte[] saltedPassword, serverKey, clientKey;
+ saltedPassword =
+ hi(
+ password.getBytes(),
+ Base64.decode(salt, Base64.DEFAULT),
+ iterations);
+ serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
+ clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
+ return new KeyPair(clientKey, serverKey);
+ });
+ }
+
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
- final HMac hMac = getHMAC();
- hMac.init(new KeyParameter(key));
- hMac.update(input, 0, input.length);
- final byte[] out = new byte[hMac.getMacSize()];
- hMac.doFinal(out, 0);
- return out;
+ return getHMac(key).hashBytes(input).asBytes();
}
- public byte[] digest(byte[] bytes) {
- final Digest digest = getDigest();
- digest.reset();
- digest.update(bytes, 0, bytes.length);
- final byte[] out = new byte[digest.getDigestSize()];
- digest.doFinal(out, 0);
- return out;
+ private byte[] digest(final byte[] bytes) {
+ return getDigest().hashBytes(bytes).asBytes();
}
/*
@@ 121,19 101,23 @@ abstract class ScramMechanism extends SaslMechanism {
}
@Override
- public String getClientFirstMessage() {
+ public String getClientFirstMessage(final SSLSocket sslSocket) {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
- clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
- ",r=" + this.clientNonce;
+ clientFirstMessageBare =
+ "n="
+ + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername()))
+ + ",r="
+ + this.clientNonce;
state = State.AUTH_TEXT_SENT;
}
return Base64.encodeToString(
- (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
+ (gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
}
@Override
- public String getResponse(final String challenge) throws AuthenticationException {
+ public String getResponse(final String challenge, final SSLSocket socket)
+ throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
if (challenge == null) {
@@ 173,7 157,8 @@ abstract class ScramMechanism extends SaslMechanism {
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
- throw new AuthenticationException("Server sent reserved token: `m'");
+ throw new AuthenticationException(
+ "Server sent reserved token: `m'");
}
}
}
@@ 182,20 167,39 @@ abstract class ScramMechanism extends SaslMechanism {
throw new AuthenticationException("Server did not send iteration count");
}
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
- throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
+ throw new AuthenticationException(
+ "Server nonce does not contain client nonce: " + nonce);
}
if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt");
}
- final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
- GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
- final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
- + clientFinalMessageWithoutProof).getBytes();
+ final byte[] channelBindingData = getChannelBindingData(socket);
+
+ final int gs2Len = this.gs2Header.getBytes().length;
+ final byte[] cMessage = new byte[gs2Len + channelBindingData.length];
+ System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len);
+ System.arraycopy(
+ channelBindingData, 0, cMessage, gs2Len, channelBindingData.length);
+
+ final String clientFinalMessageWithoutProof =
+ "c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce;
+
+ final byte[] authMessage =
+ (clientFirstMessageBare
+ + ','
+ + new String(serverFirstMessage)
+ + ','
+ + clientFinalMessageWithoutProof)
+ .getBytes();
final KeyPair keys;
try {
- keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount);
+ keys =
+ getKeyPair(
+ CryptoHelper.saslPrep(account.getPassword()),
+ salt,
+ iterationCount);
} catch (ExecutionException e) {
throw new AuthenticationException("Invalid keys generated");
}
@@ 213,35 217,77 @@ abstract class ScramMechanism extends SaslMechanism {
final byte[] clientProof = new byte[keys.clientKey.length];
if (clientSignature.length < keys.clientKey.length) {
- throw new AuthenticationException("client signature was shorter than clientKey");
+ throw new AuthenticationException(
+ "client signature was shorter than clientKey");
}
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
}
-
- final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
- Base64.encodeToString(clientProof, Base64.NO_WRAP);
+ final String clientFinalMessage =
+ clientFinalMessageWithoutProof
+ + ",p="
+ + Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT:
try {
- final String clientCalculatedServerFinalMessage = "v=" +
- Base64.encodeToString(serverSignature, Base64.NO_WRAP);
- if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
+ final String clientCalculatedServerFinalMessage =
+ "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP);
+ if (!clientCalculatedServerFinalMessage.equals(
+ new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new Exception();
}
state = State.VALID_SERVER_RESPONSE;
return "";
} catch (Exception e) {
- throw new AuthenticationException("Server final message does not match calculated final message");
+ throw new AuthenticationException(
+ "Server final message does not match calculated final message");
}
default:
throw new InvalidStateException(state);
}
}
+ protected byte[] getChannelBindingData(final SSLSocket sslSocket)
+ throws AuthenticationException {
+ if (this.channelBinding == ChannelBinding.NONE) {
+ return new byte[0];
+ }
+ throw new AssertionError("getChannelBindingData needs to be overwritten");
+ }
+
+ private static class CacheKey {
+ final String algorithm;
+ final String password;
+ final String salt;
+ final int iterations;
+
+ private CacheKey(String algorithm, String password, String salt, int iterations) {
+ this.algorithm = algorithm;
+ this.password = password;
+ this.salt = salt;
+ this.iterations = iterations;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CacheKey cacheKey = (CacheKey) o;
+ return iterations == cacheKey.iterations
+ && Objects.equal(algorithm, cacheKey.algorithm)
+ && Objects.equal(password, cacheKey.password)
+ && Objects.equal(salt, cacheKey.salt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(algorithm, password, salt, iterations);
+ }
+ }
+
private static class KeyPair {
final byte[] clientKey;
final byte[] serverKey;
A src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +23 -0
@@ 0,0 1,23 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import javax.net.ssl.SSLSocket;
+
+import eu.siacs.conversations.entities.Account;
+
+public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism {
+
+ ScramPlusMechanism(Account account, ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected byte[] getChannelBindingData(final SSLSocket sslSocket)
+ throws AuthenticationException {
+ return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
+ }
+
+ @Override
+ public ChannelBinding getChannelBinding() {
+ return this.channelBinding;
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +9 -13
@@ 1,30 1,26 @@
package eu.siacs.conversations.crypto.sasl;
-import org.bouncycastle.crypto.Digest;
-import org.bouncycastle.crypto.digests.SHA1Digest;
-import org.bouncycastle.crypto.macs.HMac;
-
-import java.security.SecureRandom;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class ScramSha1 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-1";
- @Override
- protected HMac getHMAC() {
- return new HMac(new SHA1Digest());
+ public ScramSha1(final Account account) {
+ super(account, ChannelBinding.NONE);
}
@Override
- protected Digest getDigest() {
- return new SHA1Digest();
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha1(key);
}
- public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- super(tagWriter, account, rng);
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha1();
}
@Override
A src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +35 -0
@@ 0,0 1,35 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class ScramSha1Plus extends ScramPlusMechanism {
+
+ public static final String MECHANISM = "SCRAM-SHA-1-PLUS";
+
+ public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha1(key);
+ }
+
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha1();
+ }
+
+ @Override
+ public int getPriority() {
+ return 35; // higher than SCRAM-SHA512 (30)
+ }
+
+ @Override
+ public String getMechanism() {
+ return MECHANISM;
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +10 -11
@@ 1,32 1,31 @@
package eu.siacs.conversations.crypto.sasl;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.macs.HMac;
-import java.security.SecureRandom;
-
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class ScramSha256 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-256";
- @Override
- protected HMac getHMAC() {
- return new HMac(new SHA256Digest());
+ public ScramSha256(final Account account) {
+ super(account, ChannelBinding.NONE);
}
@Override
- protected Digest getDigest() {
- return new SHA256Digest();
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha256(key);
}
- public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- super(tagWriter, account, rng);
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha256();
}
-
@Override
public int getPriority() {
return 25;
A src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +35 -0
@@ 0,0 1,35 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class ScramSha256Plus extends ScramPlusMechanism {
+
+ public static final String MECHANISM = "SCRAM-SHA-256-PLUS";
+
+ public ScramSha256Plus(final Account account, final ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha256(key);
+ }
+
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha256();
+ }
+
+ @Override
+ public int getPriority() {
+ return 40;
+ }
+
+ @Override
+ public String getMechanism() {
+ return MECHANISM;
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +10 -10
@@ 1,30 1,30 @@
package eu.siacs.conversations.crypto.sasl;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
-import java.security.SecureRandom;
-
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.TagWriter;
public class ScramSha512 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-512";
- @Override
- protected HMac getHMAC() {
- return new HMac(new SHA512Digest());
+ public ScramSha512(final Account account) {
+ super(account, ChannelBinding.NONE);
}
@Override
- protected Digest getDigest() {
- return new SHA512Digest();
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha512(key);
}
- public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
- super(tagWriter, account, rng);
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha512();
}
@Override
A src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java => src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +35 -0
@@ 0,0 1,35 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class ScramSha512Plus extends ScramPlusMechanism {
+
+ public static final String MECHANISM = "SCRAM-SHA-512-PLUS";
+
+ public ScramSha512Plus(final Account account, final ChannelBinding channelBinding) {
+ super(account, channelBinding);
+ }
+
+ @Override
+ protected HashFunction getHMac(final byte[] key) {
+ return Hashing.hmacSha512(key);
+ }
+
+ @Override
+ protected HashFunction getDigest() {
+ return Hashing.sha512();
+ }
+
+ @Override
+ public int getPriority() {
+ return 45;
+ }
+
+ @Override
+ public String getMechanism() {
+ return MECHANISM;
+ }
+}
M src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java => src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +8 -9
@@ 6,9 6,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
-/**
- * A tokenizer for GS2 header strings
- */
+/** A tokenizer for GS2 header strings */
public final class Tokenizer implements Iterator<String>, Iterable<String> {
private final List<String> parts;
private int index;
@@ 50,18 48,19 @@ public final class Tokenizer implements Iterator<String>, Iterable<String> {
}
/**
- * Removes the last object returned by {@code next} from the collection.
- * This method can only be called once between each call to {@code next}.
+ * Removes the last object returned by {@code next} from the collection. This method can only be
+ * called once between each call to {@code next}.
*
* @throws UnsupportedOperationException if removing is not supported by the collection being
- * iterated.
- * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
- * already been called after the last call to {@code next}.
+ * iterated.
+ * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
+ * already been called after the last call to {@code next}.
*/
@Override
public void remove() {
if (index <= 0) {
- throw new IllegalStateException("You can't delete an element before first next() method call");
+ throw new IllegalStateException(
+ "You can't delete an element before first next() method call");
}
parts.remove(--index);
}
M src/main/java/eu/siacs/conversations/entities/Account.java => src/main/java/eu/siacs/conversations/entities/Account.java +199 -50
@@ 6,6 6,7 @@ import android.os.SystemClock;
import android.util.Log;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import org.json.JSONException;
import org.json.JSONObject;
@@ 24,6 25,13 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.crypto.sasl.ChannelBinding;
+import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
+import eu.siacs.conversations.crypto.sasl.HashedToken;
+import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
+import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.UIHelper;
@@ 49,22 57,26 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public static final String STATUS = "status";
public static final String STATUS_MESSAGE = "status_message";
public static final String RESOURCE = "resource";
+ public static final String PINNED_MECHANISM = "pinned_mechanism";
+ public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
+ public static final String FAST_MECHANISM = "fast_mechanism";
+ public static final String FAST_TOKEN = "fast_token";
- public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
- public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
-
- public static final int OPTION_USETLS = 0;
public static final int OPTION_DISABLED = 1;
public static final int OPTION_REGISTER = 2;
- public static final int OPTION_USECOMPRESSION = 3;
public static final int OPTION_MAGIC_CREATE = 4;
public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
public static final int OPTION_UNVERIFIED = 8;
public static final int OPTION_FIXED_USERNAME = 9;
+ public static final int OPTION_QUICKSTART_AVAILABLE = 10;
+
private static final String KEY_PGP_SIGNATURE = "pgp_signature";
private static final String KEY_PGP_ID = "pgp_id";
+ private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
+ public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
+
protected final JSONObject keys;
private final Roster roster = new Roster(this);
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
@@ 90,18 102,50 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private long mEndGracePeriod = 0L;
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
private boolean bookmarksLoaded = false;
- private Presence.Status presenceStatus = Presence.Status.ONLINE;
- private String presenceStatusMessage = null;
+ private Presence.Status presenceStatus;
+ private String presenceStatusMessage;
+ private String pinnedMechanism;
+ private String pinnedChannelBinding;
+ private String fastMechanism;
+ private String fastToken;
public Account(final Jid jid, final String password) {
- this(java.util.UUID.randomUUID().toString(), jid,
- password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
- }
-
- private Account(final String uuid, final Jid jid,
- final String password, final int options, final String rosterVersion, final String keys,
- final String avatar, String displayName, String hostname, int port,
- final Presence.Status status, String statusMessage) {
+ this(
+ java.util.UUID.randomUUID().toString(),
+ jid,
+ password,
+ 0,
+ null,
+ "",
+ null,
+ null,
+ null,
+ 5222,
+ Presence.Status.ONLINE,
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ private Account(
+ final String uuid,
+ final Jid jid,
+ final String password,
+ final int options,
+ final String rosterVersion,
+ final String keys,
+ final String avatar,
+ String displayName,
+ String hostname,
+ int port,
+ final Presence.Status status,
+ String statusMessage,
+ final String pinnedMechanism,
+ final String pinnedChannelBinding,
+ final String fastMechanism,
+ final String fastToken) {
this.uuid = uuid;
this.jid = jid;
this.password = password;
@@ 120,36 164,51 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
this.port = port;
this.presenceStatus = status;
this.presenceStatusMessage = statusMessage;
+ this.pinnedMechanism = pinnedMechanism;
+ this.pinnedChannelBinding = pinnedChannelBinding;
+ this.fastMechanism = fastMechanism;
+ this.fastToken = fastToken;
}
public static Account fromCursor(final Cursor cursor) {
final Jid jid;
try {
- String resource = cursor.getString(cursor.getColumnIndex(RESOURCE));
- jid = Jid.of(
- cursor.getString(cursor.getColumnIndex(USERNAME)),
- cursor.getString(cursor.getColumnIndex(SERVER)),
- resource == null || resource.trim().isEmpty() ? null : resource);
- } catch (final IllegalArgumentException ignored) {
- Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER)));
- throw new AssertionError(ignored);
- }
- return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE));
+ jid =
+ Jid.of(
+ cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
+ cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
+ resource == null || resource.trim().isEmpty() ? null : resource);
+ } catch (final IllegalArgumentException e) {
+ Log.d(
+ Config.LOGTAG,
+ cursor.getString(cursor.getColumnIndexOrThrow(USERNAME))
+ + "@"
+ + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
+ throw new AssertionError(e);
+ }
+ return new Account(
+ cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
jid,
- cursor.getString(cursor.getColumnIndex(PASSWORD)),
- cursor.getInt(cursor.getColumnIndex(OPTIONS)),
- cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
- cursor.getString(cursor.getColumnIndex(KEYS)),
- cursor.getString(cursor.getColumnIndex(AVATAR)),
- cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
- cursor.getString(cursor.getColumnIndex(HOSTNAME)),
- cursor.getInt(cursor.getColumnIndex(PORT)),
- Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
- cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
- }
-
- public boolean httpUploadAvailable(long filesize) {
- return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
+ cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndexOrThrow(KEYS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)),
+ cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
+ cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
+ Presence.Status.fromShowString(
+ cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
+ cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
+ cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
+ cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
+ cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
+ cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
+ }
+
+ public boolean httpUploadAvailable(long size) {
+ return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size);
}
public boolean httpUploadAvailable() {
@@ 289,6 348,78 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
}
+ public void setPinnedMechanism(final SaslMechanism mechanism) {
+ this.pinnedMechanism = mechanism.getMechanism();
+ if (mechanism instanceof ChannelBindingMechanism) {
+ this.pinnedChannelBinding =
+ ((ChannelBindingMechanism) mechanism).getChannelBinding().toString();
+ } else {
+ this.pinnedChannelBinding = null;
+ }
+ }
+
+ public void setFastToken(final HashedToken.Mechanism mechanism, final String token) {
+ this.fastMechanism = mechanism.name();
+ this.fastToken = token;
+ }
+
+ public void resetFastToken() {
+ this.fastMechanism = null;
+ this.fastToken = null;
+ }
+
+ public void resetPinnedMechanism() {
+ this.pinnedMechanism = null;
+ this.pinnedChannelBinding = null;
+ setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1));
+ }
+
+ public int getPinnedMechanismPriority() {
+ final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1);
+ if (Strings.isNullOrEmpty(this.pinnedMechanism)) {
+ return fallback;
+ }
+ final SaslMechanism saslMechanism = getPinnedMechanism();
+ if (saslMechanism == null) {
+ return fallback;
+ } else {
+ return saslMechanism.getPriority();
+ }
+ }
+
+ private SaslMechanism getPinnedMechanism() {
+ final String mechanism = Strings.nullToEmpty(this.pinnedMechanism);
+ final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding);
+ return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
+ }
+
+ public HashedToken getFastMechanism() {
+ final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism);
+ final String token = this.fastToken;
+ if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
+ return null;
+ }
+ if (fastMechanism.hashFunction.equals("SHA-256")) {
+ return new HashedTokenSha256(this, fastMechanism.channelBinding);
+ } else if (fastMechanism.hashFunction.equals("SHA-512")) {
+ return new HashedTokenSha512(this, fastMechanism.channelBinding);
+ } else {
+ return null;
+ }
+ }
+
+ public SaslMechanism getQuickStartMechanism() {
+ final HashedToken hashedTokenMechanism = getFastMechanism();
+ if (hashedTokenMechanism != null) {
+ return hashedTokenMechanism;
+ }
+ return getPinnedMechanism();
+ }
+
+ public String getFastToken() {
+ return this.fastToken;
+ }
+
public State getTrueStatus() {
return this.status;
}
@@ 361,8 492,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
}
- public boolean setPrivateKeyAlias(String alias) {
- return setKey("private_key_alias", alias);
+ public void setPrivateKeyAlias(final String alias) {
+ setKey("private_key_alias", alias);
}
public String getPrivateKeyAlias() {
@@ 388,6 519,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
values.put(STATUS, presenceStatus.toShowString());
values.put(STATUS_MESSAGE, presenceStatusMessage);
values.put(RESOURCE, jid.getResource());
+ values.put(PINNED_MECHANISM, pinnedMechanism);
+ values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding);
+ values.put(FAST_MECHANISM, this.fastMechanism);
+ values.put(FAST_TOKEN, this.fastToken);
return values;
}
@@ 433,7 568,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public int activeDevicesWithRtpCapability() {
int i = 0;
- for(Presence presence : getSelfContact().getPresences().getPresences()) {
+ for (Presence presence : getSelfContact().getPresences().getPresences()) {
if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
i++;
}
@@ 490,13 625,13 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public Collection<Bookmark> getBookmarks() {
synchronized (this.bookmarks) {
- return new HashSet<>(this.bookmarks.values());
+ return ImmutableList.copyOf(this.bookmarks.values());
}
}
public boolean areBookmarksLoaded() { return bookmarksLoaded; }
- public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
+ public void setBookmarks(final Map<Jid, Bookmark> bookmarks) {
synchronized (this.bookmarks) {
this.bookmarks.clear();
this.bookmarks.putAll(bookmarks);
@@ 504,7 639,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
}
- public void putBookmark(Bookmark bookmark) {
+ public void putBookmark(final Bookmark bookmark) {
synchronized (this.bookmarks) {
this.bookmarks.put(bookmark.getJid(), bookmark);
}
@@ 573,7 708,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public String getShareableLink() {
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
- String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
+ String uri =
+ "https://conversations.im/i/"
+ + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
if (fingerprints.size() > 0) {
return XmppUri.getFingerprintUri(uri, fingerprints, '&');
} else {
@@ 586,10 723,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
if (axolotlService == null) {
return fingerprints;
}
- fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
+ fingerprints.add(
+ new XmppUri.Fingerprint(
+ XmppUri.FingerprintType.OMEMO,
+ axolotlService.getOwnFingerprint().substring(2),
+ axolotlService.getOwnDeviceId()));
for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
if (session.getTrust().isVerified() && session.getTrust().isActive()) {
- fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
+ fingerprints.add(
+ new XmppUri.Fingerprint(
+ XmppUri.FingerprintType.OMEMO,
+ session.getFingerprint().substring(2).replaceAll("\\s", ""),
+ session.getRemoteAddress().getDeviceId()));
}
}
return fingerprints;
@@ 597,7 742,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public boolean isBlocked(final ListItem contact) {
final Jid jid = contact.getJid();
- return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
+ return jid != null
+ && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
}
public boolean isBlocked(final Jid jid) {
@@ 641,11 787,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
REGISTRATION_CONFLICT(true, false),
REGISTRATION_NOT_SUPPORTED(true, false),
REGISTRATION_PLEASE_WAIT(true, false),
- REGISTRATION_INVALID_TOKEN(true,false),
+ REGISTRATION_INVALID_TOKEN(true, false),
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
TLS_ERROR,
TLS_ERROR_DOMAIN,
INCOMPATIBLE_SERVER,
+ INCOMPATIBLE_CLIENT,
TOR_NOT_AVAILABLE,
DOWNGRADE_ATTACK,
SESSION_FAILURE,
@@ 715,6 862,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return R.string.account_status_tls_error_domain;
case INCOMPATIBLE_SERVER:
return R.string.account_status_incompatible_server;
+ case INCOMPATIBLE_CLIENT:
+ return R.string.account_status_incompatible_client;
case TOR_NOT_AVAILABLE:
return R.string.account_status_tor_unavailable;
case BIND_FAILURE:
M src/main/java/eu/siacs/conversations/entities/MucOptions.java => src/main/java/eu/siacs/conversations/entities/MucOptions.java +3 -1
@@ 156,7 156,8 @@ public class MucOptions {
}
public boolean canInvite() {
- return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
+ final boolean hasPermission = !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
+ return hasPermission && online();
}
public boolean allowInvites() {
@@ 725,6 726,7 @@ public class MucOptions {
SHUTDOWN,
DESTROYED,
INVALID_NICK,
+ TECHNICAL_PROBLEMS,
UNKNOWN,
NON_ANONYMOUS
}
M src/main/java/eu/siacs/conversations/generator/IqGenerator.java => src/main/java/eu/siacs/conversations/generator/IqGenerator.java +14 -7
@@ 145,8 145,8 @@ public class IqGenerator extends AbstractGenerator {
return publish(Namespace.NICK, item);
}
- public IqPacket deleteNode(String node) {
- IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ public IqPacket deleteNode(final String node) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
pubsub.addChild("delete").setAttribute("node", node);
return packet;
@@ 165,9 165,9 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket publishAvatar(Avatar avatar, Bundle options) {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
- final Element data = item.addChild("data", "urn:xmpp:avatar:data");
+ final Element data = item.addChild("data", Namespace.AVATAR_DATA);
data.setContent(avatar.image);
- return publish("urn:xmpp:avatar:data", item, options);
+ return publish(Namespace.AVATAR_DATA, item, options);
}
public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) {
@@ 181,20 181,20 @@ public class IqGenerator extends AbstractGenerator {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
final Element metadata = item
- .addChild("metadata", "urn:xmpp:avatar:metadata");
+ .addChild("metadata", Namespace.AVATAR_METADATA);
final Element info = metadata.addChild("info");
info.setAttribute("bytes", avatar.size);
info.setAttribute("id", avatar.sha1sum);
info.setAttribute("height", avatar.height);
info.setAttribute("width", avatar.height);
info.setAttribute("type", avatar.type);
- return publish("urn:xmpp:avatar:metadata", item, options);
+ return publish(Namespace.AVATAR_METADATA, item, options);
}
public IqPacket retrievePepAvatar(final Avatar avatar) {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
- final IqPacket packet = retrieve("urn:xmpp:avatar:data", item);
+ final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item);
packet.setTo(avatar.owner);
return packet;
}
@@ 206,6 206,13 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
+ public IqPacket retrieveVcardAvatar(final Jid to) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ packet.setTo(to);
+ packet.addChild("vCard", "vcard-temp");
+ return packet;
+ }
+
public IqPacket retrieveAvatarMetaData(final Jid to) {
final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
if (to != null) {
M src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java => src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +3 -1
@@ 1,5 1,7 @@
package eu.siacs.conversations.http;
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
import android.os.Build;
import android.util.Log;
@@ 147,7 149,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive();
}
try {
- final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
+ final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM);
builder.sslSocketFactory(sf, trustManager);
builder.hostnameVerifier(new StrictHostnameVerifier());
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
M src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java => src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +3 -1
@@ 1,5 1,7 @@
package eu.siacs.conversations.http;
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
import android.util.Log;
import androidx.annotation.NonNull;
@@ 124,7 126,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[44];
- mXmppConnectionService.getRNG().nextBytes(this.key);
+ SECURE_RANDOM.nextBytes(this.key);
this.file.setKeyAndIv(this.key);
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
M src/main/java/eu/siacs/conversations/parser/MessageParser.java => src/main/java/eu/siacs/conversations/parser/MessageParser.java +6 -4
@@ 236,7 236,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
- AxolotlService axolotlService = account.getAxolotlService();
+ final AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
} else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
@@ 282,6 282,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
account.setBookmarks(Collections.emptyMap());
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
+ } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) {
+ Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node");
}
}
@@ 314,7 316,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
private boolean handleErrorMessage(final Account account, final MessagePacket packet) {
if (packet.getType() == MessagePacket.TYPE_ERROR) {
if (packet.fromServer(account)) {
- final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
+ final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS);
if (forwarded != null) {
return handleErrorMessage(account, forwarded.first);
}
@@ 390,8 392,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return;
} else if (original.fromServer(account)) {
Pair<MessagePacket, Long> f;
- f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
- f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
+ f = original.getForwardedMessagePacket("received", Namespace.CARBONS);
+ f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f;
packet = f != null ? f.first : original;
if (handleErrorMessage(account, packet)) {
return;
M src/main/java/eu/siacs/conversations/parser/PresenceParser.java => src/main/java/eu/siacs/conversations/parser/PresenceParser.java +16 -4
@@ 56,7 56,8 @@ public class PresenceParser extends AbstractParser implements
}
private void processConferencePresence(PresencePacket packet, Conversation conversation) {
- MucOptions mucOptions = conversation.getMucOptions();
+ final Account account = conversation.getAccount();
+ final MucOptions mucOptions = conversation.getMucOptions();
final Jid jid = conversation.getAccount().getJid();
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
@@ 93,7 94,7 @@ public class PresenceParser extends AbstractParser implements
axolotlService.fetchDeviceIds(user.getRealJid());
}
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
- Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().asBareJid()
+ Log.d(Config.LOGTAG,account.getJid().asBareJid()
+": room '"
+mucOptions.getConversation().getJid().asBareJid()
+"' created. pushing default configuration");
@@ 138,13 139,24 @@ public class PresenceParser extends AbstractParser implements
final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid"));
mucOptions.setError(MucOptions.Error.DESTROYED);
if (alternate != null) {
- Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate);
}
} else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) {
mucOptions.setError(MucOptions.Error.SHUTDOWN);
} else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) {
if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) {
- mucOptions.setError(MucOptions.Error.UNKNOWN);
+ final boolean wasOnline = mucOptions.online();
+ mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": received status code 333 in room "
+ + mucOptions.getConversation().getJid().asBareJid()
+ + " online="
+ + wasOnline);
+ if (wasOnline) {
+ mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ }
} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
mucOptions.setError(MucOptions.Error.KICKED);
} else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +43 -37
@@ 67,7 67,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 49;
+ private static final int DATABASE_VERSION = 51;
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
@@ 294,6 294,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Account.KEYS + " TEXT, "
+ Account.HOSTNAME + " TEXT, "
+ Account.RESOURCE + " TEXT,"
+ + Account.PINNED_MECHANISM + " TEXT,"
+ + Account.PINNED_CHANNEL_BINDING + " TEXT,"
+ + Account.FAST_MECHANISM + " TEXT,"
+ + Account.FAST_TOKEN + " TEXT,"
+ Account.PORT + " NUMBER DEFAULT 5222)");
db.execSQL("create table " + Conversation.TABLENAME + " ("
+ Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
@@ 653,6 657,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.endTransaction();
requiresMessageIndexRebuild = true;
}
+ if (oldVersion < 50 && newVersion >= 50) {
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT");
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT");
+ }
+ if (oldVersion < 51 && newVersion >= 51) {
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT");
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT");
+ }
}
private void canonicalizeJids(SQLiteDatabase db) {
@@ 1034,20 1046,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
contactJid.asBareJid().toString() + "/%",
contactJid.asBareJid().toString()
};
- Cursor cursor = db.query(Conversation.TABLENAME, null,
+ try(final Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
- + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
- if (cursor.getCount() == 0) {
- cursor.close();
- return null;
- }
- cursor.moveToFirst();
- Conversation conversation = Conversation.fromCursor(cursor);
- cursor.close();
- if (conversation.getJid() instanceof InvalidJid) {
- return null;
+ + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null)) {
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ final Conversation conversation = Conversation.fromCursor(cursor);
+ if (conversation.getJid() instanceof InvalidJid) {
+ return null;
+ }
+ return conversation;
}
- return conversation;
}
public void updateConversation(final Conversation conversation) {
@@ 1063,33 1074,28 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
public List<Jid> getAccountJids(final boolean enabledOnly) {
- SQLiteDatabase db = this.getReadableDatabase();
+ final SQLiteDatabase db = this.getReadableDatabase();
final List<Jid> jids = new ArrayList<>();
final String[] columns = new String[]{Account.USERNAME, Account.SERVER};
- String where = enabledOnly ? "not options & (1 <<1)" : null;
- Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null);
- try {
- while (cursor.moveToNext()) {
+ final String where = enabledOnly ? "not options & (1 <<1)" : null;
+ try (final Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null));
}
+ } catch (final Exception e) {
return jids;
- } catch (Exception e) {
- return jids;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
+ return jids;
}
private List<Account> getAccounts(SQLiteDatabase db) {
- List<Account> list = new ArrayList<>();
- Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
- null, null);
- while (cursor.moveToNext()) {
- list.add(Account.fromCursor(cursor));
+ final List<Account> list = new ArrayList<>();
+ try (final Cursor cursor =
+ db.query(Account.TABLENAME, null, null, null, null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ list.add(Account.fromCursor(cursor));
+ }
}
- cursor.close();
return list;
}
@@ 1127,14 1133,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
public void readRoster(Roster roster) {
- SQLiteDatabase db = this.getReadableDatabase();
- Cursor cursor;
- String[] args = {roster.getAccount().getUuid()};
- cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null);
- while (cursor.moveToNext()) {
- roster.initContact(Contact.fromCursor(cursor));
+ final SQLiteDatabase db = this.getReadableDatabase();
+ final String[] args = {roster.getAccount().getUuid()};
+ try (final Cursor cursor =
+ db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
+ while (cursor.moveToNext()) {
+ roster.initContact(Contact.fromCursor(cursor));
+ }
}
- cursor.close();
}
public void writeRoster(final Roster roster) {
M src/main/java/eu/siacs/conversations/persistance/FileBackend.java => src/main/java/eu/siacs/conversations/persistance/FileBackend.java +9 -10
@@ 694,7 694,7 @@ public class FileBackend {
} catch (final FileWriterException e) {
cleanup(file);
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
- } catch (final SecurityException e) {
+ } catch (final SecurityException | IllegalStateException e) {
cleanup(file);
throw new FileCopyException(R.string.error_security_exception);
} catch (final IOException e) {
@@ 1687,19 1687,19 @@ public class FileBackend {
return 0;
}
return Integer.parseInt(value);
- } catch (final IllegalArgumentException e) {
+ } catch (final Exception e) {
return 0;
}
}
private Dimensions getImageDimensions(File file) {
- BitmapFactory.Options options = new BitmapFactory.Options();
+ final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- int rotation = getRotation(file);
- boolean rotated = rotation == 90 || rotation == 270;
- int imageHeight = rotated ? options.outWidth : options.outHeight;
- int imageWidth = rotated ? options.outHeight : options.outWidth;
+ final int rotation = getRotation(file);
+ final boolean rotated = rotation == 90 || rotation == 270;
+ final int imageHeight = rotated ? options.outWidth : options.outHeight;
+ final int imageWidth = rotated ? options.outHeight : options.outWidth;
return new Dimensions(imageHeight, imageWidth);
}
@@ 1713,7 1713,6 @@ public class FileBackend {
return getVideoDimensions(metadataRetriever);
}
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private Dimensions getPdfDocumentDimensions(final File file) {
final ParcelFileDescriptor fileDescriptor;
try {
@@ 1721,7 1720,7 @@ public class FileBackend {
if (fileDescriptor == null) {
return new Dimensions(0, 0);
}
- } catch (FileNotFoundException e) {
+ } catch (final FileNotFoundException e) {
return new Dimensions(0, 0);
}
try {
@@ 1732,7 1731,7 @@ public class FileBackend {
page.close();
pdfRenderer.close();
return scalePdfDimensions(new Dimensions(height, width));
- } catch (IOException | SecurityException e) {
+ } catch (final IOException | SecurityException e) {
Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e);
return new Dimensions(0, 0);
}
M src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java => src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +21 -2
@@ 33,6 33,7 @@ import java.util.concurrent.CountDownLatch;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils;
+import eu.siacs.conversations.xmpp.jingle.Media;
/**
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
@@ 44,7 45,7 @@ public class AppRTCAudioManager {
private final Context apprtcContext;
// Contains speakerphone setting: auto, true or false
@Nullable
- private final SpeakerPhonePreference speakerPhonePreference;
+ private SpeakerPhonePreference speakerPhonePreference;
// Handles all tasks related to Bluetooth headset devices.
private final AppRTCBluetoothManager bluetoothManager;
@Nullable
@@ 110,6 111,16 @@ public class AppRTCAudioManager {
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
}
+ public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
+ this.speakerPhonePreference = speakerPhonePreference;
+ if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
+ defaultAudioDevice = AudioDevice.EARPIECE;
+ } else {
+ defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ }
+ updateAudioDeviceState();
+ }
+
/**
* Construction.
*/
@@ 587,7 598,15 @@ public class AppRTCAudioManager {
}
public enum SpeakerPhonePreference {
- AUTO, EARPIECE, SPEAKER
+ AUTO, EARPIECE, SPEAKER;
+
+ public static SpeakerPhonePreference of(final Set<Media> media) {
+ if (media.contains(Media.VIDEO)) {
+ return SPEAKER;
+ } else {
+ return EARPIECE;
+ }
+ }
}
/**
M src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java => src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +125 -83
@@ 4,6 4,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
+import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
@@ 39,7 40,6 @@ public class ChannelDiscoveryService {
private final XmppConnectionService service;
-
private MuclumbusService muclumbusService;
private final Cache<String, List<Room>> cache;
@@ 50,16 50,21 @@ public class ChannelDiscoveryService {
}
void initializeMuclumbusService() {
+ if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
+ this.muclumbusService = null;
+ return;
+ }
final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
if (service.useTorToConnect()) {
builder.proxy(HttpConnectionManager.getProxy());
}
- Retrofit retrofit = new Retrofit.Builder()
- .client(builder.build())
- .baseUrl(Config.CHANNEL_DISCOVERY)
- .addConverterFactory(GsonConverterFactory.create())
- .callbackExecutor(Executors.newSingleThreadExecutor())
- .build();
+ final Retrofit retrofit =
+ new Retrofit.Builder()
+ .client(builder.build())
+ .baseUrl(Config.CHANNEL_DISCOVERY)
+ .addConverterFactory(GsonConverterFactory.create())
+ .callbackExecutor(Executors.newSingleThreadExecutor())
+ .build();
this.muclumbusService = retrofit.create(MuclumbusService.class);
}
@@ 67,7 72,10 @@ public class ChannelDiscoveryService {
cache.invalidateAll();
}
- void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) {
+ void discover(
+ @NonNull final String query,
+ Method method,
+ OnChannelSearchResultsFound onChannelSearchResultsFound) {
final List<Room> result = cache.getIfPresent(key(method, query));
if (result != null) {
onChannelSearchResultsFound.onChannelSearchResultsFound(result);
@@ 84,59 92,82 @@ public class ChannelDiscoveryService {
}
}
- private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) {
- Call<MuclumbusService.Rooms> call = muclumbusService.getRooms(1);
- try {
- call.enqueue(new Callback<MuclumbusService.Rooms>() {
- @Override
- public void onResponse(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Response<MuclumbusService.Rooms> response) {
- final MuclumbusService.Rooms body = response.body();
- if (body == null) {
- listener.onChannelSearchResultsFound(Collections.emptyList());
- logError(response);
- return;
+ private void discoverChannelsJabberNetwork(final OnChannelSearchResultsFound listener) {
+ if (muclumbusService == null) {
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ return;
+ }
+ final Call<MuclumbusService.Rooms> call = muclumbusService.getRooms(1);
+ call.enqueue(
+ new Callback<MuclumbusService.Rooms>() {
+ @Override
+ public void onResponse(
+ @NonNull Call<MuclumbusService.Rooms> call,
+ @NonNull Response<MuclumbusService.Rooms> response) {
+ final MuclumbusService.Rooms body = response.body();
+ if (body == null) {
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ logError(response);
+ return;
+ }
+ cache.put(key(Method.JABBER_NETWORK, ""), body.items);
+ listener.onChannelSearchResultsFound(body.items);
}
- cache.put(key(Method.JABBER_NETWORK, ""), body.items);
- listener.onChannelSearchResultsFound(body.items);
- }
- @Override
- public void onFailure(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Throwable throwable) {
- Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
- listener.onChannelSearchResultsFound(Collections.emptyList());
- }
- });
- } catch (Exception e) {
- e.printStackTrace();
- }
+ @Override
+ public void onFailure(
+ @NonNull Call<MuclumbusService.Rooms> call,
+ @NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
+ throwable);
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ }
+ });
}
- private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) {
- MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query);
- Call<MuclumbusService.SearchResult> searchResultCall = muclumbusService.search(searchRequest);
-
- searchResultCall.enqueue(new Callback<MuclumbusService.SearchResult>() {
- @Override
- public void onResponse(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Response<MuclumbusService.SearchResult> response) {
- final MuclumbusService.SearchResult body = response.body();
- if (body == null) {
- listener.onChannelSearchResultsFound(Collections.emptyList());
- logError(response);
- return;
- }
- cache.put(key(Method.JABBER_NETWORK, query), body.result.items);
- listener.onChannelSearchResultsFound(body.result.items);
- }
+ private void discoverChannelsJabberNetwork(
+ final String query, final OnChannelSearchResultsFound listener) {
+ if (muclumbusService == null) {
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ return;
+ }
+ final MuclumbusService.SearchRequest searchRequest =
+ new MuclumbusService.SearchRequest(query);
+ final Call<MuclumbusService.SearchResult> searchResultCall =
+ muclumbusService.search(searchRequest);
+ searchResultCall.enqueue(
+ new Callback<MuclumbusService.SearchResult>() {
+ @Override
+ public void onResponse(
+ @NonNull Call<MuclumbusService.SearchResult> call,
+ @NonNull Response<MuclumbusService.SearchResult> response) {
+ final MuclumbusService.SearchResult body = response.body();
+ if (body == null) {
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ logError(response);
+ return;
+ }
+ cache.put(key(Method.JABBER_NETWORK, query), body.result.items);
+ listener.onChannelSearchResultsFound(body.result.items);
+ }
- @Override
- public void onFailure(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Throwable throwable) {
- Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
- listener.onChannelSearchResultsFound(Collections.emptyList());
- }
- });
+ @Override
+ public void onFailure(
+ @NonNull Call<MuclumbusService.SearchResult> call,
+ @NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
+ throwable);
+ listener.onChannelSearchResultsFound(Collections.emptyList());
+ }
+ });
}
- private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) {
+ private void discoverChannelsLocalServers(
+ final String query, final OnChannelSearchResultsFound listener) {
final Map<Jid, Account> localMucService = getLocalMucServices();
Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
if (localMucService.size() == 0) {
@@ 156,38 187,49 @@ public class ChannelDiscoveryService {
for (Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
queriesInFlight.incrementAndGet();
- service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> {
- if (itemsResponse.getType() == IqPacket.TYPE.RESULT) {
- final List<Jid> items = IqParser.items(itemsResponse);
- for (Jid item : items) {
- IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item);
- queriesInFlight.incrementAndGet();
- service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() {
- @Override
- public void onIqPacketReceived(Account account, IqPacket infoResponse) {
- if (infoResponse.getType() == IqPacket.TYPE.RESULT) {
- final Room room = IqParser.parseRoom(infoResponse);
- if (room != null) {
- rooms.add(room);
- }
- if (queriesInFlight.decrementAndGet() <= 0) {
- finishDiscoSearch(rooms, query, listener);
- }
- } else {
- queriesInFlight.decrementAndGet();
- }
+ service.sendIqPacket(
+ entry.getValue(),
+ itemsRequest,
+ (account, itemsResponse) -> {
+ if (itemsResponse.getType() == IqPacket.TYPE.RESULT) {
+ final List<Jid> items = IqParser.items(itemsResponse);
+ for (Jid item : items) {
+ IqPacket infoRequest =
+ service.getIqGenerator().queryDiscoInfo(item);
+ queriesInFlight.incrementAndGet();
+ service.sendIqPacket(
+ account,
+ infoRequest,
+ new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(
+ Account account, IqPacket infoResponse) {
+ if (infoResponse.getType()
+ == IqPacket.TYPE.RESULT) {
+ final Room room =
+ IqParser.parseRoom(infoResponse);
+ if (room != null) {
+ rooms.add(room);
+ }
+ if (queriesInFlight.decrementAndGet() <= 0) {
+ finishDiscoSearch(rooms, query, listener);
+ }
+ } else {
+ queriesInFlight.decrementAndGet();
+ }
+ }
+ });
}
- });
- }
- }
- if (queriesInFlight.decrementAndGet() <= 0) {
- finishDiscoSearch(rooms, query, listener);
- }
- });
+ }
+ if (queriesInFlight.decrementAndGet() <= 0) {
+ finishDiscoSearch(rooms, query, listener);
+ }
+ });
}
}
- private void finishDiscoSearch(List<Room> rooms, String query, OnChannelSearchResultsFound listener) {
+ private void finishDiscoSearch(
+ List<Room> rooms, String query, OnChannelSearchResultsFound listener) {
Collections.sort(rooms);
cache.put(key(Method.LOCAL_SERVER, ""), rooms);
if (query.isEmpty()) {
@@ 241,7 283,7 @@ public class ChannelDiscoveryService {
try {
Log.d(Config.LOGTAG, "error body=" + errorBody.string());
} catch (IOException e) {
- //ignored
+ // ignored
}
}
M src/main/java/eu/siacs/conversations/services/MessageArchiveService.java => src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +3 -1
@@ 1,5 1,7 @@
package eu.siacs.conversations.services;
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
import android.util.Log;
import org.jetbrains.annotations.NotNull;
@@ 502,7 504,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.start = start.getTimestamp();
}
this.end = end;
- this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
+ this.queryId = new BigInteger(50, SECURE_RANDOM).toString(32);
this.version = version;
}
M src/main/java/eu/siacs/conversations/services/NotificationService.java => src/main/java/eu/siacs/conversations/services/NotificationService.java +122 -84
@@ 42,6 42,7 @@ import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
+import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
@@ 104,12 105,13 @@ public class NotificationService {
private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
- private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
- public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14;
+ public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
+ private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13;
private final XmppConnectionService mXmppConnectionService;
private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
- private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls = new LinkedHashMap<>();
+ private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls =
+ new LinkedHashMap<>();
private Conversation mOpenConversation;
private boolean mIsInForeground;
private long mLastNotification;
@@ 230,9 232,11 @@ public class NotificationService {
ongoingCallsChannel.setGroup("calls");
notificationManager.createNotificationChannel(ongoingCallsChannel);
- final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls",
- c.getString(R.string.missed_calls_channel_name),
- NotificationManager.IMPORTANCE_HIGH);
+ final NotificationChannel missedCallsChannel =
+ new NotificationChannel(
+ "missed_calls",
+ c.getString(R.string.missed_calls_channel_name),
+ NotificationManager.IMPORTANCE_HIGH);
missedCallsChannel.setShowBadge(true);
missedCallsChannel.setSound(null, null);
missedCallsChannel.setLightColor(LED_COLOR);
@@ 419,8 423,8 @@ public class NotificationService {
return count;
}
- void finishBacklog(boolean notify) {
- finishBacklog(notify, null);
+ void finishBacklog() {
+ finishBacklog(false, null);
}
private void pushToStack(final Message message) {
@@ 927,7 931,8 @@ public class NotificationService {
singleBuilder.setGroupAlertBehavior(
NotificationCompat.GROUP_ALERT_SUMMARY);
}
- modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences);
+ modifyForSoundVibrationAndLight(
+ singleBuilder, notifyThis, quiteHours, preferences);
singleBuilder.setGroup(MESSAGES_GROUP);
setNotificationColor(singleBuilder);
notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
@@ 1031,30 1036,39 @@ public class NotificationService {
}
private Builder buildMissedCallsSummary(boolean publicVersion) {
- final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
+ final Builder builder =
+ new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
int totalCalls = 0;
- final StringBuilder names = new StringBuilder();
+ final List<String> names = new ArrayList<>();
long lastTime = 0;
- for (Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
+ for (final Map.Entry<Conversational, MissedCallsInfo> entry : mMissedCalls.entrySet()) {
final Conversational conversation = entry.getKey();
final MissedCallsInfo missedCallsInfo = entry.getValue();
- names.append(conversation.getContact().getDisplayName());
- names.append(", ");
+ names.add(conversation.getContact().getDisplayName());
totalCalls += missedCallsInfo.getNumberOfCalls();
lastTime = Math.max(lastTime, missedCallsInfo.getLastTime());
}
- if (names.length() >= 2) {
- names.delete(names.length() - 2, names.length());
- }
- final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) :
- (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) :
- mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size());
+ final String title =
+ (totalCalls == 1)
+ ? mXmppConnectionService.getString(R.string.missed_call)
+ : (mMissedCalls.size() == 1)
+ ? mXmppConnectionService
+ .getResources()
+ .getQuantityString(
+ R.plurals.n_missed_calls, totalCalls, totalCalls)
+ : mXmppConnectionService
+ .getResources()
+ .getQuantityString(
+ R.plurals.n_missed_calls_from_m_contacts,
+ mMissedCalls.size(),
+ totalCalls,
+ mMissedCalls.size());
builder.setContentTitle(title);
builder.setTicker(title);
if (!publicVersion) {
- builder.setContentText(names.toString());
+ builder.setContentText(Joiner.on(", ").join(names));
}
- builder.setSmallIcon(R.drawable.ic_missed_call_notification);
+ builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
builder.setGroupSummary(true);
builder.setGroup(MISSED_CALLS_GROUP);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
@@ 1076,38 1090,55 @@ public class NotificationService {
return builder.build();
}
- private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
- final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
- final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) :
- mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls());
+ private Builder buildMissedCall(
+ final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) {
+ final Builder builder =
+ new NotificationCompat.Builder(mXmppConnectionService, "missed_calls");
+ final String title =
+ (info.getNumberOfCalls() == 1)
+ ? mXmppConnectionService.getString(R.string.missed_call)
+ : mXmppConnectionService
+ .getResources()
+ .getQuantityString(
+ R.plurals.n_missed_calls,
+ info.getNumberOfCalls(),
+ info.getNumberOfCalls());
builder.setContentTitle(title);
final String name = conversation.getContact().getDisplayName();
if (publicVersion) {
builder.setTicker(title);
} else {
- if (info.getNumberOfCalls() == 1) {
- builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name));
- } else {
- builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name));
- }
+ builder.setTicker(
+ mXmppConnectionService
+ .getResources()
+ .getQuantityString(
+ R.plurals.n_missed_calls_from_x,
+ info.getNumberOfCalls(),
+ info.getNumberOfCalls(),
+ name));
builder.setContentText(name);
}
- builder.setSmallIcon(R.drawable.ic_missed_call_notification);
+ builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
builder.setGroup(MISSED_CALLS_GROUP);
builder.setCategory(NotificationCompat.CATEGORY_CALL);
builder.setWhen(info.getLastTime());
builder.setContentIntent(createContentIntent(conversation));
builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation));
if (!publicVersion && conversation instanceof Conversation) {
- builder.setLargeIcon(mXmppConnectionService.getAvatarService()
- .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
+ builder.setLargeIcon(
+ mXmppConnectionService
+ .getAvatarService()
+ .get(
+ (Conversation) conversation,
+ AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
}
modifyMissedCall(builder);
return builder;
}
private void modifyMissedCall(final Builder builder) {
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
+ final SharedPreferences preferences =
+ PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
final Resources resources = mXmppConnectionService.getResources();
final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
if (led) {
@@ 1131,42 1162,39 @@ public class NotificationService {
R.plurals.x_unread_conversations,
notifications.size(),
notifications.size()));
- final StringBuilder names = new StringBuilder();
+ final List<String> names = new ArrayList<>();
Conversation conversation = null;
for (final ArrayList<Message> messages : notifications.values()) {
- if (messages.size() > 0) {
- conversation = (Conversation) messages.get(0).getConversation();
- final String name = conversation.getName().toString();
- SpannableString styledString;
- if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
- int count = messages.size();
- styledString =
- new SpannableString(
- name
- + ": "
- + mXmppConnectionService
- .getResources()
- .getQuantityString(
- R.plurals.x_messages, count, count));
- styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
- style.addLine(styledString);
- } else {
- styledString =
- new SpannableString(
- name
- + ": "
- + UIHelper.getMessagePreview(
- mXmppConnectionService, messages.get(0))
- .first);
- styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
- style.addLine(styledString);
- }
- names.append(name);
- names.append(", ");
+ if (messages.isEmpty()) {
+ continue;
}
- }
- if (names.length() >= 2) {
- names.delete(names.length() - 2, names.length());
+ conversation = (Conversation) messages.get(0).getConversation();
+ final String name = conversation.getName().toString();
+ SpannableString styledString;
+ if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+ int count = messages.size();
+ styledString =
+ new SpannableString(
+ name
+ + ": "
+ + mXmppConnectionService
+ .getResources()
+ .getQuantityString(
+ R.plurals.x_messages, count, count));
+ styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+ style.addLine(styledString);
+ } else {
+ styledString =
+ new SpannableString(
+ name
+ + ": "
+ + UIHelper.getMessagePreview(
+ mXmppConnectionService, messages.get(0))
+ .first);
+ styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
+ style.addLine(styledString);
+ }
+ names.add(name);
}
final String contentTitle =
mXmppConnectionService
@@ 1177,7 1205,7 @@ public class NotificationService {
notifications.size());
mBuilder.setContentTitle(contentTitle);
mBuilder.setTicker(contentTitle);
- mBuilder.setContentText(names.toString());
+ mBuilder.setContentText(Joiner.on(", ").join(names));
mBuilder.setStyle(style);
if (conversation != null) {
mBuilder.setContentIntent(createContentIntent(conversation));
@@ 1582,7 1610,7 @@ public class NotificationService {
return createContentIntent(conversation.getUuid(), null);
}
- private PendingIntent createDeleteIntent(Conversation conversation) {
+ private PendingIntent createDeleteIntent(final Conversation conversation) {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION);
if (conversation != null) {
@@ 1609,11 1637,21 @@ public class NotificationService {
intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION);
if (conversation != null) {
intent.putExtra("uuid", conversation.getUuid());
- return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent,
- s() ? PendingIntent.FLAG_IMMUTABLE : 0);
+ return PendingIntent.getService(
+ mXmppConnectionService,
+ generateRequestCode(conversation, 21),
+ intent,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
}
- return PendingIntent.getService(mXmppConnectionService, 1, intent,
- s() ? PendingIntent.FLAG_IMMUTABLE : 0);
+ return PendingIntent.getService(
+ mXmppConnectionService,
+ 1,
+ intent,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createReplyIntent(
@@ 1937,16 1975,6 @@ public class NotificationService {
}
}
- private class VibrationRunnable implements Runnable {
-
- @Override
- public void run() {
- final Vibrator vibrator =
- (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
- vibrator.vibrate(CALL_PATTERN, -1);
- }
- }
-
private static class MissedCallsInfo {
private int numberOfCalls;
private long lastTime;
@@ 1969,4 1997,14 @@ public class NotificationService {
return lastTime;
}
}
+
+ private class VibrationRunnable implements Runnable {
+
+ @Override
+ public void run() {
+ final Vibrator vibrator =
+ (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
+ vibrator.vibrate(CALL_PATTERN, -1);
+ }
+ }
}
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +86 -26
@@ 1,6 1,7 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.utils.Compatibility.s;
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.Manifest;
import android.annotation.SuppressLint;
@@ 40,7 41,6 @@ import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.security.KeyChain;
import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.DisplayMetrics;
@@ 50,6 50,7 @@ import android.util.Pair;
import androidx.annotation.BoolRes;
import androidx.annotation.IntegerRes;
+import androidx.annotation.NonNull;
import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
@@ 392,7 393,6 @@ public class XmppConnectionService extends Service {
}
};
private final AtomicLong mLastExpiryRun = new AtomicLong(0);
- private SecureRandom mRandom;
private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnStatusChanged statusListener = new OnStatusChanged() {
@@ 464,7 464,7 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");