aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java81
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java24
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java53
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java189
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java76
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java7
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java65
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Presences.java40
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java3
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java91
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java631
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java7
-rw-r--r--src/main/res/values-de/strings.xml15
-rw-r--r--src/main/res/values-it/strings.xml4
-rw-r--r--src/main/res/values/strings.xml2
15 files changed, 862 insertions, 426 deletions
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java
new file mode 100644
index 00000000..b56d2a46
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java
@@ -0,0 +1,81 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class DigestMd5 extends SaslMechanism {
+ public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
+ super(tagWriter, account, rng);
+ }
+
+ public static String getMechanism() {
+ return "DIGEST-MD5";
+ }
+
+ private State state = State.INITIAL;
+
+ @Override
+ public String getResponse(final String challenge) throws AuthenticationException {
+ switch (state) {
+ case INITIAL:
+ state = State.RESPONSE_SENT;
+ final String encodedResponse;
+ try {
+ final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
+ String nonce = "";
+ for (final String token : tokenizer) {
+ final String[] parts = token.split("=");
+ if (parts[0].equals("nonce")) {
+ nonce = parts[1].replace("\"", "");
+ } else if (parts[0].equals("rspauth")) {
+ return "";
+ }
+ }
+ final String digestUri = "xmpp/" + account.getServer();
+ final String nonceCount = "00000001";
+ 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 = new BigInteger(100, rng).toString(32);
+ 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);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new AuthenticationException(e);
+ }
+
+ return encodedResponse;
+ case RESPONSE_SENT:
+ state = State.VALID_SERVER_RESPONSE;
+ break;
+ default:
+ throw new InvalidStateException(state);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java
new file mode 100644
index 00000000..f7e7ee8a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java
@@ -0,0 +1,24 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.nio.charset.Charset;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class Plain extends SaslMechanism {
+ public Plain(final TagWriter tagWriter, final Account account) {
+ super(tagWriter, account, null);
+ }
+
+ public static String getMechanism() {
+ return "PLAIN";
+ }
+
+ @Override
+ public String getClientFirstMessage() {
+ final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword();
+ return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
new file mode 100644
index 00000000..7dd5e99c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
@@ -0,0 +1,53 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public abstract class SaslMechanism {
+
+ final protected TagWriter tagWriter;
+ final protected Account account;
+ final protected SecureRandom rng;
+
+ protected static enum State {
+ INITIAL,
+ AUTH_TEXT_SENT,
+ RESPONSE_SENT,
+ VALID_SERVER_RESPONSE,
+ }
+
+ public static class AuthenticationException extends Exception {
+ public AuthenticationException(final String message) {
+ super(message);
+ }
+
+ public AuthenticationException(final Exception inner) {
+ super(inner);
+ }
+ }
+
+ public static class InvalidStateException extends AuthenticationException {
+ public InvalidStateException(final String message) {
+ super(message);
+ }
+
+ public InvalidStateException(final State state) {
+ this("Invalid state: " + state.toString());
+ }
+ }
+
+ public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
+ this.tagWriter = tagWriter;
+ this.account = account;
+ this.rng = rng;
+ }
+
+ public String getClientFirstMessage() {
+ return "";
+ }
+ public String getResponse(final String challenge) throws AuthenticationException {
+ return "";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
new file mode 100644
index 00000000..2073de2d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
@@ -0,0 +1,189 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+import org.bouncycastle.crypto.macs.HMac;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class ScramSha1 extends SaslMechanism {
+ // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
+ final private static String GS2_HEADER = "n,,";
+ private String clientFirstMessageBare;
+ private byte[] serverFirstMessage;
+ final private String clientNonce;
+ private byte[] serverSignature = null;
+ private static HMac HMAC;
+ private static Digest DIGEST;
+ private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
+ private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
+
+ static {
+ DIGEST = new SHA1Digest();
+ HMAC = new HMac(new SHA1Digest());
+ }
+
+ private State state = State.INITIAL;
+
+ public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
+ super(tagWriter, account, rng);
+
+ // This nonce should be different for each authentication attempt.
+ clientNonce = new BigInteger(100, this.rng).toString(32);
+ clientFirstMessageBare = "";
+ }
+
+ public static String getMechanism() {
+ return "SCRAM-SHA-1";
+ }
+
+ @Override
+ public String getClientFirstMessage() {
+ if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
+ clientFirstMessageBare = "n=" + CryptoHelper.saslPrep(account.getUsername()) +
+ ",r=" + this.clientNonce;
+ state = State.AUTH_TEXT_SENT;
+ }
+ return Base64.encodeToString(
+ (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ }
+
+ @Override
+ public String getResponse(final String challenge) throws AuthenticationException {
+ switch (state) {
+ case AUTH_TEXT_SENT:
+ serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
+ final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
+ String nonce = "";
+ int iterationCount = -1;
+ String salt = "";
+ for (final String token : tokenizer) {
+ if (token.charAt(1) == '=') {
+ switch (token.charAt(0)) {
+ case 'i':
+ try {
+ iterationCount = Integer.parseInt(token.substring(2));
+ } catch (final NumberFormatException e) {
+ throw new AuthenticationException(e);
+ }
+ break;
+ case 's':
+ salt = token.substring(2);
+ break;
+ case 'r':
+ nonce = token.substring(2);
+ break;
+ case 'm':
+ /*
+ * RFC 5802:
+ * m: This attribute is reserved for future extensibility. In this
+ * version of SCRAM, its presence in a client or a server message
+ * MUST cause authentication failure when the attribute is parsed by
+ * the other end.
+ */
+ throw new AuthenticationException("Server sent reserved token: `m'");
+ }
+ }
+ }
+
+ if (iterationCount < 0) {
+ 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);
+ }
+ 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();
+
+ // TODO: In future, cache the clientKey and serverKey and re-use them on re-auth.
+ final byte[] saltedPassword, clientSignature, serverKey, clientKey;
+ try {
+ saltedPassword = hi(CryptoHelper.saslPrep(account.getPassword()).getBytes(),
+ Base64.decode(salt, Base64.DEFAULT), iterationCount);
+ serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
+ serverSignature = hmac(serverKey, authMessage);
+ clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
+ final byte[] storedKey = digest(clientKey);
+
+ clientSignature = hmac(storedKey, authMessage);
+
+ } catch (final InvalidKeyException e) {
+ throw new AuthenticationException(e);
+ }
+
+ final byte[] clientProof = new byte[clientKey.length];
+
+ for (int i = 0; i < clientProof.length; i++) {
+ clientProof[i] = (byte) (clientKey[i] ^ clientSignature[i]);
+ }
+
+
+ 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:
+ final String clientCalculatedServerFinalMessage = "v=" +
+ Base64.encodeToString(serverSignature, Base64.NO_WRAP);
+ if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
+ throw new AuthenticationException("Server final message does not match calculated final message");
+ }
+ state = State.VALID_SERVER_RESPONSE;
+ return "";
+ default:
+ throw new InvalidStateException(state);
+ }
+ }
+
+ public static synchronized byte[] hmac(final byte[] key, final byte[] input)
+ throws InvalidKeyException {
+ HMAC.init(new KeyParameter(key));
+ HMAC.update(input, 0, input.length);
+ final byte[] out = new byte[HMAC.getMacSize()];
+ HMAC.doFinal(out, 0);
+ return out;
+ }
+
+ public static synchronized byte[] digest(byte[] bytes) {
+ DIGEST.reset();
+ DIGEST.update(bytes, 0, bytes.length);
+ final byte[] out = new byte[DIGEST.getDigestSize()];
+ DIGEST.doFinal(out, 0);
+ return out;
+ }
+
+ /*
+ * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
+ * pseudorandom function (PRF) and with dkLen == output length of
+ * HMAC() == output length of H().
+ */
+ private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
+ throws InvalidKeyException {
+ byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
+ byte[] out = u.clone();
+ for (int i = 1; i < iterations; i++) {
+ u = hmac(key, u);
+ for (int j = 0; j < u.length; j++) {
+ out[j] ^= u[j];
+ }
+ }
+ return out;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java
new file mode 100644
index 00000000..4797e6e8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * A tokenizer for GS2 header strings
+ */
+public final class Tokenizer implements Iterator<String>, Iterable<String> {
+ private final List<String> parts;
+ private int index;
+
+ public Tokenizer(final byte[] challenge) {
+ final String challengeString = new String(challenge);
+ parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
+ index = 0;
+ }
+
+ /**
+ * Returns true if there is at least one more element, false otherwise.
+ *
+ * @see #next
+ */
+ @Override
+ public boolean hasNext() {
+ return parts.size() != index + 1;
+ }
+
+ /**
+ * Returns the next object and advances the iterator.
+ *
+ * @return the next object.
+ * @throws java.util.NoSuchElementException if there are no more elements.
+ * @see #hasNext
+ */
+ @Override
+ public String next() {
+ if (hasNext()) {
+ return parts.get(index++);
+ } else {
+ throw new NoSuchElementException("No such element. Size is: " + parts.size());
+ }
+ }
+
+ /**
+ * 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}.
+ */
+ @Override
+ public void remove() {
+ if(index <= 0) {
+ throw new IllegalStateException("You can't delete an element before first next() method call");
+ }
+ parts.remove(--index);
+ }
+
+ /**
+ * Returns an {@link java.util.Iterator} for the elements in this object.
+ *
+ * @return An {@code Iterator} instance.
+ */
+ @Override
+ public Iterator<String> iterator() {
+ return parts.iterator();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index 0a9e5da2..5b44435e 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -96,7 +96,12 @@ public class Message extends AbstractEntity {
public static Message fromCursor(Cursor cursor) {
Jid jid;
try {
- jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART)));
+ String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
+ if (value!=null) {
+ jid = Jid.fromString(value);
+ } else {
+ jid = null;
+ }
} catch (InvalidJidException e) {
jid = null;
}
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index e25c6b89..6eb1d43c 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -158,39 +158,42 @@ public class MucOptions {
String type = packet.getAttribute("type");
if (type == null) {
User user = new User();
- Element item = packet.findChild("x",
- "http://jabber.org/protocol/muc#user")
- .findChild("item");
- user.setName(name);
- user.setAffiliation(item.getAttribute("affiliation"));
- user.setRole(item.getAttribute("role"));
- user.setJid(item.getAttribute("jid"));
- user.setName(name);
- if (name.equals(this.joinnick)) {
- this.isOnline = true;
- this.error = ERROR_NO_ERROR;
- self = user;
- if (aboutToRename) {
- if (renameListener != null) {
- renameListener.onRename(true);
- }
- aboutToRename = false;
- }
- } else {
- addUser(user);
- }
- if (pgp != null) {
- Element x = packet.findChild("x", "jabber:x:signed");
- if (x != null) {
- Element status = packet.findChild("status");
- String msg;
- if (status != null) {
- msg = status.getContent();
+ Element x = packet.findChild("x","http://jabber.org/protocol/muc#user");
+ if (x != null) {
+ Element item = x.findChild("item");
+ if (item != null) {
+ user.setName(name);
+ user.setAffiliation(item.getAttribute("affiliation"));
+ user.setRole(item.getAttribute("role"));
+ user.setJid(item.getAttribute("jid"));
+ user.setName(name);
+ if (name.equals(this.joinnick)) {
+ this.isOnline = true;
+ this.error = ERROR_NO_ERROR;
+ self = user;
+ if (aboutToRename) {
+ if (renameListener != null) {
+ renameListener.onRename(true);
+ }
+ aboutToRename = false;
+ }
} else {
- msg = "";
+ addUser(user);
+ }
+ if (pgp != null) {
+ Element signed = packet.findChild("x", "jabber:x:signed");
+ if (signed != null) {
+ Element status = packet.findChild("status");
+ String msg;
+ if (status != null) {
+ msg = status.getContent();
+ } else {
+ msg = "";
+ }
+ user.setPgpKeyId(pgp.fetchKeyId(account, msg,
+ signed.getContent()));
+ }
}
- user.setPgpKeyId(pgp.fetchKeyId(account, msg,
- x.getContent()));
}
}
} else if (type.equals("unavailable") && name.equals(this.joinnick)) {
diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java
index b5899847..bccf3117 100644
--- a/src/main/java/eu/siacs/conversations/entities/Presences.java
+++ b/src/main/java/eu/siacs/conversations/entities/Presences.java
@@ -22,24 +22,32 @@ public class Presences {
}
public void updatePresence(String resource, int status) {
- this.presences.put(resource, status);
+ synchronized (this.presences) {
+ this.presences.put(resource, status);
+ }
}
public void removePresence(String resource) {
- this.presences.remove(resource);
+ synchronized (this.presences) {
+ this.presences.remove(resource);
+ }
}
public void clearPresences() {
- this.presences.clear();
+ synchronized (this.presences) {
+ this.presences.clear();
+ }
}
public int getMostAvailableStatus() {
int status = OFFLINE;
- Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
- while (it.hasNext()) {
- Entry<String, Integer> entry = it.next();
- if (entry.getValue() < status)
- status = entry.getValue();
+ synchronized (this.presences) {
+ Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, Integer> entry = it.next();
+ if (entry.getValue() < status)
+ status = entry.getValue();
+ }
}
return status;
}
@@ -61,16 +69,22 @@ public class Presences {
}
public int size() {
- return presences.size();
+ synchronized (this.presences) {
+ return presences.size();
+ }
}
public String[] asStringArray() {
- final String[] presencesArray = new String[presences.size()];
- presences.keySet().toArray(presencesArray);
- return presencesArray;
+ synchronized (this.presences) {
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ return presencesArray;
+ }
}
public boolean has(String presence) {
- return presences.containsKey(presence);
+ synchronized (this.presences) {
+ return presences.containsKey(presence);
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 6defd91c..bc2db87f 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -133,6 +133,9 @@ public class MessageParser extends AbstractParser implements
private Message parseGroupchat(MessagePacket packet, Account account) {
int status;
final Jid from = packet.getFrom();
+ if (from == null) {
+ return null;
+ }
if (mXmppConnectionService.find(account.pendingConferenceLeaves,
account, from.toBareJid()) != null) {
return null;
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
index 47595c6e..bcc54a26 100644
--- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -1,20 +1,14 @@
package eu.siacs.conversations.utils;
-import java.math.BigInteger;
-import java.nio.charset.Charset;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
-
-import eu.siacs.conversations.entities.Account;
-import android.util.Base64;
+import java.text.Normalizer;
public class CryptoHelper {
public static final String FILETRANSFER = "?FILETRANSFERv1:";
final protected static char[] hexArray = "0123456789abcdef".toCharArray();
final protected static char[] vowels = "aeiou".toCharArray();
- final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz"
- .toCharArray();
+ final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray();
+ final public static byte[] ONE = new byte[] { 0, 0, 0, 1 };
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
@@ -36,64 +30,13 @@ public class CryptoHelper {
return array;
}
- public static String saslPlain(String username, String password) {
- String sasl = '\u0000' + username + '\u0000' + password;
- return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()),
- Base64.NO_WRAP);
- }
-
- private static byte[] concatenateByteArrays(byte[] a, byte[] b) {
+ public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
byte[] result = new byte[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
- public static String saslDigestMd5(Account account, String challenge,
- SecureRandom random) {
- try {
- String[] challengeParts = new String(Base64.decode(challenge,
- Base64.DEFAULT)).split(",");
- String nonce = "";
- for (int i = 0; i < challengeParts.length; ++i) {
- String[] parts = challengeParts[i].split("=");
- if (parts[0].equals("nonce")) {
- nonce = parts[1].replace("\"", "");
- } else if (parts[0].equals("rspauth")) {
- return null;
- }
- }
- String digestUri = "xmpp/" + account.getServer();
- String nonceCount = "00000001";
- String x = account.getUsername() + ":" + account.getServer() + ":"
- + account.getPassword();
- MessageDigest md = MessageDigest.getInstance("MD5");
- byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
- String cNonce = new BigInteger(100, random).toString(32);
- byte[] a1 = concatenateByteArrays(y,
- (":" + nonce + ":" + cNonce).getBytes(Charset
- .defaultCharset()));
- String a2 = "AUTHENTICATE:" + digestUri;
- String ha1 = bytesToHex(md.digest(a1));
- String ha2 = bytesToHex(md.digest(a2.getBytes(Charset
- .defaultCharset())));
- String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
- + ":auth:" + ha2;
- String response = bytesToHex(md.digest(kd.getBytes(Charset
- .defaultCharset())));
- String saslString = "username=\"" + account.getUsername()
- + "\",realm=\"" + account.getServer() + "\",nonce=\""
- + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
- + ",qop=auth,digest-uri=\"" + digestUri + "\",response="
- + response + ",charset=utf-8";
- return Base64.encodeToString(
- saslString.getBytes(Charset.defaultCharset()),
- Base64.NO_WRAP);
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
- }
-
public static String randomMucName(SecureRandom random) {
return randomWord(3, random) + "." + randomWord(7, random);
}
@@ -109,4 +52,30 @@ public class CryptoHelper {
}
return builder.toString();
}
+
+ /**
+ * Escapes usernames or passwords for SASL.
+ */
+ public static String saslEscape(final String s) {
+ final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1));
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case ',':
+ sb.append("=2C");
+ break;
+ case '=':
+ sb.append("=3D");
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String saslPrep(final String s) {
+ return saslEscape(Normalizer.normalize(s, Normalizer.Form.NFKC));
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 4bd3668a..c34a08a8 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -39,9 +39,12 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.sasl.DigestMd5;
+import eu.siacs.conversations.crypto.sasl.Plain;
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+import eu.siacs.conversations.crypto.sasl.ScramSha1;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.DNSHelper;
import eu.siacs.conversations.utils.zlib.ZLibInputStream;
import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
@@ -104,10 +107,12 @@ public class XmppConnection implements Runnable {
private OnMessageAcknowledged acknowledgedListener = null;
private XmppConnectionService mXmppConnectionService = null;
+ private SaslMechanism saslMechanism;
+
public XmppConnection(Account account, XmppConnectionService service) {
this.account = account;
this.wakeLock = service.getPowerManager().newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
+ PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
tagWriter = new TagWriter();
mXmppConnectionService = service;
applicationContext = service.getApplicationContext();
@@ -120,7 +125,7 @@ public class XmppConnection implements Runnable {
&& (account.getStatus() != Account.STATUS_ONLINE)
&& (account.getStatus() != Account.STATUS_DISABLED)) {
return;
- }
+ }
if (nextStatus == Account.STATUS_ONLINE) {
this.attempt = 0;
}
@@ -140,7 +145,7 @@ public class XmppConnection implements Runnable {
this.attempt++;
try {
shouldAuthenticate = shouldBind = !account
- .isOptionSet(Account.OPTION_REGISTER);
+ .isOptionSet(Account.OPTION_REGISTER);
tagReader = new XmlReader(wakeLock);
tagWriter = new TagWriter();
packetCallbacks.clear();
@@ -158,12 +163,12 @@ public class XmppConnection implements Runnable {
Bundle namePort = (Bundle) values.get(i);
try {
String srvRecordServer;
- try {
- srvRecordServer=IDN.toASCII(namePort.getString("name"));
- } catch (final IllegalArgumentException e) {
- // TODO: Handle me?`
- srvRecordServer = "";
- }
+ try {
+ srvRecordServer=IDN.toASCII(namePort.getString("name"));
+ } catch (final IllegalArgumentException e) {
+ // TODO: Handle me?`
+ srvRecordServer = "";
+ }
int srvRecordPort = namePort.getInt("port");
String srvIpServer = namePort.getString("ipv4");
InetSocketAddress addr;
@@ -236,7 +241,7 @@ public class XmppConnection implements Runnable {
} catch (final RuntimeException ignored) {
}
}
- } catch (final IOException | XmlPullParserException e) {
+ } catch (final IOException | XmlPullParserException e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
this.changeStatus(Account.STATUS_OFFLINE);
if (wakeLock.isHeld()) {
@@ -245,7 +250,7 @@ public class XmppConnection implements Runnable {
} catch (final RuntimeException ignored) {
}
}
- } catch (NoSuchAlgorithmException e) {
+ } catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
this.changeStatus(Account.STATUS_OFFLINE);
Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
@@ -255,9 +260,9 @@ public class XmppConnection implements Runnable {
} catch (final RuntimeException ignored) {
}
}
- }
+ }
- }
+ }
@Override
public void run() {
@@ -265,116 +270,127 @@ public class XmppConnection implements Runnable {
}
private void processStream(final Tag currentTag) throws XmlPullParserException,
- IOException, NoSuchAlgorithmException {
- Tag nextTag = tagReader.readTag();
- while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
- if (nextTag.isStart("error")) {
- processStreamError(nextTag);
- } else if (nextTag.isStart("features")) {
- processStreamFeatures(nextTag);
- } else if (nextTag.isStart("proceed")) {
- switchOverToTls(nextTag);
- } else if (nextTag.isStart("compressed")) {
- switchOverToZLib(nextTag);
- } else if (nextTag.isStart("success")) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
- tagReader.readTag();
- tagReader.reset();
- sendStartStream();
- processStream(tagReader.readTag());
- break;
- } else if (nextTag.isStart("failure")) {
- tagReader.readElement(nextTag);
- changeStatus(Account.STATUS_UNAUTHORIZED);
- } else if (nextTag.isStart("challenge")) {
- String challange = tagReader.readElement(nextTag).getContent();
- Element response = new Element("response");
- response.setAttribute("xmlns",
- "urn:ietf:params:xml:ns:xmpp-sasl");
- response.setContent(CryptoHelper.saslDigestMd5(account,
- challange, mXmppConnectionService.getRNG()));
- tagWriter.writeElement(response);
- } else if (nextTag.isStart("enabled")) {
- Element enabled = tagReader.readElement(nextTag);
- if ("true".equals(enabled.getAttribute("resume"))) {
- this.streamId = enabled.getAttribute("id");
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": stream managment(" + smVersion
- + ") enabled (resumable)");
- } else {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": stream managment(" + smVersion + ") enabled");
- }
- this.lastSessionStarted = SystemClock.elapsedRealtime();
- this.stanzasReceived = 0;
- RequestPacket r = new RequestPacket(smVersion);
- tagWriter.writeStanzaAsync(r);
- } else if (nextTag.isStart("resumed")) {
- lastPaketReceived = SystemClock.elapsedRealtime();
- Element resumed = tagReader.readElement(nextTag);
- String h = resumed.getAttribute("h");
- try {
- int serverCount = Integer.parseInt(h);
- if (serverCount != stanzasSent) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": session resumed with lost packages");
- stanzasSent = serverCount;
- } else {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": session resumed");
- }
- if (acknowledgedListener != null) {
- for (int i = 0; i < messageReceipts.size(); ++i) {
- if (serverCount >= messageReceipts.keyAt(i)) {
- acknowledgedListener.onMessageAcknowledged(
- account, messageReceipts.valueAt(i));
+ IOException, NoSuchAlgorithmException {
+ Tag nextTag = tagReader.readTag();
+
+ while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
+ if (nextTag.isStart("error")) {
+ processStreamError(nextTag);
+ } else if (nextTag.isStart("features")) {
+ processStreamFeatures(nextTag);
+ } else if (nextTag.isStart("proceed")) {
+ switchOverToTls(nextTag);
+ } else if (nextTag.isStart("compressed")) {
+ switchOverToZLib(nextTag);
+ } else if (nextTag.isStart("success")) {
+ final String challenge = tagReader.readElement(nextTag).getContent();
+ try {
+ saslMechanism.getResponse(challenge);
+ } catch (final SaslMechanism.AuthenticationException e) {
+ disconnect(true);
+ Log.e(Config.LOGTAG, String.valueOf(e));
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
+ tagReader.reset();
+ sendStartStream();
+ processStream(tagReader.readTag());
+ break;
+ } else if (nextTag.isStart("failure")) {
+ tagReader.readElement(nextTag);
+ changeStatus(Account.STATUS_UNAUTHORIZED);
+ } else if (nextTag.isStart("challenge")) {
+ final String challenge = tagReader.readElement(nextTag).getContent();
+ final Element response = new Element("response");
+ response.setAttribute("xmlns",
+ "urn:ietf:params:xml:ns:xmpp-sasl");
+ try {
+ response.setContent(saslMechanism.getResponse(challenge));
+ } catch (final SaslMechanism.AuthenticationException e) {
+ // TODO: Send auth abort tag.
+ Log.e(Config.LOGTAG, e.toString());
+ }
+ tagWriter.writeElement(response);
+ } else if (nextTag.isStart("enabled")) {
+ Element enabled = tagReader.readElement(nextTag);
+ if ("true".equals(enabled.getAttribute("resume"))) {
+ this.streamId = enabled.getAttribute("id");
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": stream managment(" + smVersion
+ + ") enabled (resumable)");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": stream managment(" + smVersion + ") enabled");
+ }
+ this.lastSessionStarted = SystemClock.elapsedRealtime();
+ this.stanzasReceived = 0;
+ RequestPacket r = new RequestPacket(smVersion);
+ tagWriter.writeStanzaAsync(r);
+ } else if (nextTag.isStart("resumed")) {
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ Element resumed = tagReader.readElement(nextTag);
+ String h = resumed.getAttribute("h");
+ try {
+ int serverCount = Integer.parseInt(h);
+ if (serverCount != stanzasSent) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": session resumed with lost packages");
+ stanzasSent = serverCount;
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": session resumed");
+ }
+ if (acknowledgedListener != null) {
+ for (int i = 0; i < messageReceipts.size(); ++i) {
+ if (serverCount >= messageReceipts.keyAt(i)) {
+ acknowledgedListener.onMessageAcknowledged(
+ account, messageReceipts.valueAt(i));
+ }
+ }
+ }
+ messageReceipts.clear();
+ } catch (final NumberFormatException ignored) {
+
+ }
+ sendInitialPing();
+
+ } else if (nextTag.isStart("r")) {
+ tagReader.readElement(nextTag);
+ AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+ tagWriter.writeStanzaAsync(ack);
+ } else if (nextTag.isStart("a")) {
+ Element ack = tagReader.readElement(nextTag);
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+ String msgId = this.messageReceipts.get(serverSequence);
+ if (msgId != null) {
+ if (this.acknowledgedListener != null) {
+ this.acknowledgedListener.onMessageAcknowledged(
+ account, msgId);
+ }
+ this.messageReceipts.remove(serverSequence);
+ }
+ } else if (nextTag.isStart("failed")) {
+ tagReader.readElement(nextTag);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
+ streamId = null;
+ if (account.getStatus() != Account.STATUS_ONLINE) {
+ sendBindRequest();
+ }
+ } else if (nextTag.isStart("iq")) {
+ processIq(nextTag);
+ } else if (nextTag.isStart("message")) {
+ processMessage(nextTag);
+ } else if (nextTag.isStart("presence")) {
+ processPresence(nextTag);
+ }
+ nextTag = tagReader.readTag();
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ account. setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
}
}
- }
- messageReceipts.clear();
- } catch (final NumberFormatException ignored) {
-
- }
- sendInitialPing();
-
- } else if (nextTag.isStart("r")) {
- tagReader.readElement(nextTag);
- AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
- tagWriter.writeStanzaAsync(ack);
- } else if (nextTag.isStart("a")) {
- Element ack = tagReader.readElement(nextTag);
- lastPaketReceived = SystemClock.elapsedRealtime();
- int serverSequence = Integer.parseInt(ack.getAttribute("h"));
- String msgId = this.messageReceipts.get(serverSequence);
- if (msgId != null) {
- if (this.acknowledgedListener != null) {
- this.acknowledgedListener.onMessageAcknowledged(
- account, msgId);
- }
- this.messageReceipts.remove(serverSequence);
- }
- } else if (nextTag.isStart("failed")) {
- tagReader.readElement(nextTag);
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
- streamId = null;
- if (account.getStatus() != Account.STATUS_ONLINE) {
- sendBindRequest();
- }
- } else if (nextTag.isStart("iq")) {
- processIq(nextTag);
- } else if (nextTag.isStart("message")) {
- processMessage(nextTag);
- } else if (nextTag.isStart("presence")) {
- processPresence(nextTag);
- }
- nextTag = tagReader.readTag();
- }
- if (account.getStatus() == Account.STATUS_ONLINE) {
- account. setStatus(Account.STATUS_OFFLINE);
- if (statusListener != null) {
- statusListener.onStatusChanged(account);
- }
- }
}
private void sendInitialPing() {
@@ -394,7 +410,7 @@ public class XmppConnection implements Runnable {
}
private Element processPacket(Tag currentTag, int packetType)
- throws XmlPullParserException, IOException {
+ throws XmlPullParserException, IOException {
Element element;
switch (packetType) {
case PACKET_IQ:
@@ -421,10 +437,10 @@ public class XmppConnection implements Runnable {
if (packetType == PACKET_IQ
&& "jingle".equals(child.getName())
&& ("set".equalsIgnoreCase(type) || "get"
- .equalsIgnoreCase(type))) {
+ .equalsIgnoreCase(type))) {
element = new JinglePacket();
element.setAttributes(currentTag.getAttributes());
- }
+ }
element.addChild(child);
}
nextTag = tagReader.readTag();
@@ -438,64 +454,64 @@ public class XmppConnection implements Runnable {
}
private void processIq(Tag currentTag) throws XmlPullParserException,
- IOException {
- IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
-
- if (packet.getId() == null) {
- return; // an iq packet without id is definitely invalid
- }
+ IOException {
+ IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
- if (packet instanceof JinglePacket) {
- if (this.jingleListener != null) {
- this.jingleListener.onJinglePacketReceived(account,
- (JinglePacket) packet);
- }
- } else {
- if (packetCallbacks.containsKey(packet.getId())) {
- if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
- ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
- .onIqPacketReceived(account, packet);
- }
+ if (packet.getId() == null) {
+ return; // an iq packet without id is definitely invalid
+ }
- packetCallbacks.remove(packet.getId());
- } else if ((packet.getType() == IqPacket.TYPE_GET || packet
- .getType() == IqPacket.TYPE_SET)
- && this.unregisteredIqListener != null) {
- this.unregisteredIqListener.onIqPacketReceived(account, packet);
- }
- }
+ if (packet instanceof JinglePacket) {
+ if (this.jingleListener != null) {
+ this.jingleListener.onJinglePacketReceived(account,
+ (JinglePacket) packet);
+ }
+ } else {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
+ ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
+ .onIqPacketReceived(account, packet);
+ }
+
+ packetCallbacks.remove(packet.getId());
+ } else if ((packet.getType() == IqPacket.TYPE_GET || packet
+ .getType() == IqPacket.TYPE_SET)
+ && this.unregisteredIqListener != null) {
+ this.unregisteredIqListener.onIqPacketReceived(account, packet);
+ }
+ }
}
private void processMessage(Tag currentTag) throws XmlPullParserException,
- IOException {
- MessagePacket packet = (MessagePacket) processPacket(currentTag,
- PACKET_MESSAGE);
- String id = packet.getAttribute("id");
- if ((id != null) && (packetCallbacks.containsKey(id))) {
- if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
- ((OnMessagePacketReceived) packetCallbacks.get(id))
- .onMessagePacketReceived(account, packet);
- }
- packetCallbacks.remove(id);
- } else if (this.messageListener != null) {
- this.messageListener.onMessagePacketReceived(account, packet);
- }
+ IOException {
+ MessagePacket packet = (MessagePacket) processPacket(currentTag,
+ PACKET_MESSAGE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
+ ((OnMessagePacketReceived) packetCallbacks.get(id))
+ .onMessagePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.messageListener != null) {
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
}
private void processPresence(Tag currentTag) throws XmlPullParserException,
- IOException {
- PresencePacket packet = (PresencePacket) processPacket(currentTag,
- PACKET_PRESENCE);
- String id = packet.getAttribute("id");
- if ((id != null) && (packetCallbacks.containsKey(id))) {
- if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
- ((OnPresencePacketReceived) packetCallbacks.get(id))
- .onPresencePacketReceived(account, packet);
- }
- packetCallbacks.remove(id);
- } else if (this.presenceListener != null) {
- this.presenceListener.onPresencePacketReceived(account, packet);
- }
+ IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag,
+ PACKET_PRESENCE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
+ ((OnPresencePacketReceived) packetCallbacks.get(id))
+ .onPresencePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.presenceListener != null) {
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
}
private void sendCompressionZlib() throws IOException {
@@ -506,18 +522,18 @@ public class XmppConnection implements Runnable {
}
private void switchOverToZLib(final Tag currentTag)
- throws XmlPullParserException, IOException,
- NoSuchAlgorithmException {
- tagReader.readTag(); // read tag close
- tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
- .getOutputStream()));
- tagReader
- .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
-
- sendStartStream();
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": compression enabled");
- usingCompression = true;
- processStream(tagReader.readTag());
+ throws XmlPullParserException, IOException,
+ NoSuchAlgorithmException {
+ tagReader.readTag(); // read tag close
+ tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
+ .getOutputStream()));
+ tagReader
+ .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
+
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": compression enabled");
+ usingCompression = true;
+ processStream(tagReader.readTag());
}
private void sendStartTLS() throws IOException {
@@ -528,7 +544,7 @@ public class XmppConnection implements Runnable {
private SharedPreferences getPreferences() {
return PreferenceManager
- .getDefaultSharedPreferences(applicationContext);
+ .getDefaultSharedPreferences(applicationContext);
}
private boolean enableLegacySSL() {
@@ -536,81 +552,64 @@ public class XmppConnection implements Runnable {
}
private void switchOverToTls(final Tag currentTag) throws XmlPullParserException,
- IOException {
- tagReader.readTag();
- try {
- SSLContext sc = SSLContext.getInstance("TLS");
- sc.init(null,
- new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},
- mXmppConnectionService.getRNG());
- SSLSocketFactory factory = sc.getSocketFactory();
-
- if (factory == null) {
- throw new IOException("SSLSocketFactory was null");
- }
-
- final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier());
-
- if (socket == null) {
- throw new IOException("socket was null");
- }
- final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
- socket.getInetAddress().getHostAddress(), socket.getPort(),
- true);
-
- // Support all protocols except legacy SSL.
- // The min SDK version prevents us having to worry about SSLv2. In
- // future, this may be true of SSLv3 as well.
- final String[] supportProtocols;
- if (enableLegacySSL()) {
- supportProtocols = sslSocket.getSupportedProtocols();
- } else {
- final List<String> supportedProtocols = new LinkedList<>(
- Arrays.asList(sslSocket.getSupportedProtocols()));
- supportedProtocols.remove("SSLv3");
- supportProtocols = new String[supportedProtocols.size()];
- supportedProtocols.toArray(supportProtocols);
- }
- sslSocket.setEnabledProtocols(supportProtocols);
+ IOException {
+ tagReader.readTag();
+ try {
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null,
+ new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},
+ mXmppConnectionService.getRNG());
+ SSLSocketFactory factory = sc.getSocketFactory();
+
+ if (factory == null) {
+ throw new IOException("SSLSocketFactory was null");
+ }
- if (verifier != null
- && !verifier.verify(account.getServer().getDomainpart(),
- sslSocket.getSession())) {
- sslSocket.close();
- throw new IOException("host mismatch in TLS connection");
- }
- tagReader.setInputStream(sslSocket.getInputStream());
- tagWriter.setOutputStream(sslSocket.getOutputStream());
- sendStartStream();
- Log.d(Config.LOGTAG, account.getJid().toBareJid()
- + ": TLS connection established");
- usingEncryption = true;
- processStream(tagReader.readTag());
- sslSocket.close();
- } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
- e1.printStackTrace();
- }
- }
-
- private void sendSaslAuthPlain() throws IOException {
- String saslString = CryptoHelper.saslPlain(account.getUsername(),
- account.getPassword());
- Element auth = new Element("auth");
- auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
- auth.setAttribute("mechanism", "PLAIN");
- auth.setContent(saslString);
- tagWriter.writeElement(auth);
- }
+ final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier());
- private void sendSaslAuthDigestMd5() throws IOException {
- Element auth = new Element("auth");
- auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
- auth.setAttribute("mechanism", "DIGEST-MD5");
- tagWriter.writeElement(auth);
+ if (socket == null) {
+ throw new IOException("socket was null");
+ }
+ final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
+ socket.getInetAddress().getHostAddress(), socket.getPort(),
+ true);
+
+ // Support all protocols except legacy SSL.
+ // The min SDK version prevents us having to worry about SSLv2. In
+ // future, this may be true of SSLv3 as well.
+ final String[] supportProtocols;
+ if (enableLegacySSL()) {
+ supportProtocols = sslSocket.getSupportedProtocols();
+ } else {
+ final List<String> supportedProtocols = new LinkedList<>(
+ Arrays.asList(sslSocket.getSupportedProtocols()));
+ supportedProtocols.remove("SSLv3");
+ supportProtocols = new String[supportedProtocols.size()];
+ supportedProtocols.toArray(supportProtocols);
+ }
+ sslSocket.setEnabledProtocols(supportProtocols);
+
+ if (verifier != null
+ && !verifier.verify(account.getServer().getDomainpart(),
+ sslSocket.getSession())) {
+ sslSocket.close();
+ throw new IOException("host mismatch in TLS connection");
+ }
+ tagReader.setInputStream(sslSocket.getInputStream());
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": TLS connection established");
+ usingEncryption = true;
+ processStream(tagReader.readTag());
+ sslSocket.close();
+ } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
+ e1.printStackTrace();
+ }
}
private void processStreamFeatures(Tag currentTag)
- throws XmlPullParserException, IOException {
+ throws XmlPullParserException, IOException {
this.streamFeatures = tagReader.readElement(currentTag);
if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
sendStartTLS();
@@ -626,15 +625,29 @@ public class XmppConnection implements Runnable {
disconnect(true);
} else if (this.streamFeatures.hasChild("mechanisms")
&& shouldAuthenticate && usingEncryption) {
- List<String> mechanisms = extractMechanisms(streamFeatures
+ final List<String> mechanisms = extractMechanisms(streamFeatures
.findChild("mechanisms"));
- if (mechanisms.contains("PLAIN")) {
- sendSaslAuthPlain();
- } else if (mechanisms.contains("DIGEST-MD5")) {
- sendSaslAuthDigestMd5();
+ final Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ if (mechanisms.contains(ScramSha1.getMechanism())) {
+ saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
+ Log.d(Config.LOGTAG, "Authenticating with " + ScramSha1.getMechanism());
+ auth.setAttribute("mechanism", ScramSha1.getMechanism());
+ } else if (mechanisms.contains(DigestMd5.getMechanism())) {
+ Log.d(Config.LOGTAG, "Authenticating with " + DigestMd5.getMechanism());
+ saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
+ auth.setAttribute("mechanism", DigestMd5.getMechanism());
+ } else if (mechanisms.contains(Plain.getMechanism())) {
+ Log.d(Config.LOGTAG, "Authenticating with " + Plain.getMechanism());
+ saslMechanism = new Plain(tagWriter, account);
+ auth.setAttribute("mechanism", Plain.getMechanism());
}
+ if (!saslMechanism.getClientFirstMessage().isEmpty()) {
+ auth.setContent(saslMechanism.getClientFirstMessage());
+ }
+ tagWriter.writeElement(auth);
} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
- + smVersion)
+ + smVersion)
&& streamId != null) {
ResumePacket resume = new ResumePacket(this.streamId,
stanzasReceived, smVersion);
@@ -650,7 +663,7 @@ public class XmppConnection implements Runnable {
private boolean compressionAvailable() {
if (!this.streamFeatures.hasChild("compression",
- "http://jabber.org/features/compress"))
+ "http://jabber.org/features/compress"))
return false;
if (!ZLibOutputStream.SUPPORTED)
return false;
@@ -692,23 +705,23 @@ public class XmppConnection implements Runnable {
&& (packet.query().hasChild("password"))) {
IqPacket register = new IqPacket(IqPacket.TYPE_SET);
Element username = new Element("username")
- .setContent(account.getUsername());
+ .setContent(account.getUsername());
Element password = new Element("password")
- .setContent(account.getPassword());
+ .setContent(account.getPassword());
register.query("jabber:iq:register").addChild(username);
register.query().addChild(password);
sendIqPacket(register, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account,
- IqPacket packet) {
+ IqPacket packet) {
if (packet.getType() == IqPacket.TYPE_RESULT) {
account.setOption(Account.OPTION_REGISTER,
false);
changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
} else if (packet.hasChild("error")
&& (packet.findChild("error")
- .hasChild("conflict"))) {
+ .hasChild("conflict"))) {
changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
} else {
changeStatus(Account.STATUS_REGISTRATION_FAILED);
@@ -731,7 +744,7 @@ public class XmppConnection implements Runnable {
private void sendBindRequest() throws IOException {
IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
- .addChild("resource").setContent(account.getResource());
+ .addChild("resource").setContent(account.getResource());
this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@@ -739,19 +752,19 @@ public class XmppConnection implements Runnable {
if (bind != null) {
final Element jid = bind.findChild("jid");
if (jid != null && jid.getContent() != null) {
- try {
- account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
- } catch (final InvalidJidException e) {
- // TODO: Handle the case where an external JID is technically invalid?
- }
- if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+ try {
+ account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
+ } catch (final InvalidJidException e) {
+ // TODO: Handle the case where an external JID is technically invalid?
+ }
+ if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
smVersion = 3;
EnablePacket enable = new EnablePacket(smVersion);
tagWriter.writeStanzaAsync(enable);
stanzasSent = 0;
messageReceipts.clear();
} else if (streamFeatures.hasChild("sm",
- "urn:xmpp:sm:2")) {
+ "urn:xmpp:sm:2")) {
smVersion = 2;
EnablePacket enable = new EnablePacket(smVersion);
tagWriter.writeStanzaAsync(enable);
@@ -792,11 +805,11 @@ public class XmppConnection implements Runnable {
public void onIqPacketReceived(Account account, IqPacket packet) {
final List<Element> elements = packet.query().getChildren();
final List<String> features = new ArrayList<>();
- for (Element element : elements) {
- if (element.getName().equals("feature")) {
- features.add(element.getAttribute("var"));
- }
- }
+ for (Element element : elements) {
+ if (element.getName().equals("feature")) {
+ features.add(element.getAttribute("var"));
+ }
+ }
disco.put(server.toDomainJid().toString(), features);
if (account.getServer().equals(server.toDomainJid())) {
@@ -821,16 +834,16 @@ public class XmppConnection implements Runnable {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
List<Element> elements = packet.query().getChildren();
- for (Element element : elements) {
- if (element.getName().equals("item")) {
- final String jid = element.getAttribute("jid");
- try {
- sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid());
- } catch (final InvalidJidException ignored) {
- // TODO: Handle the case where an external JID is technically invalid?
- }
- }
- }
+ for (Element element : elements) {
+ if (element.getName().equals("item")) {
+ final String jid = element.getAttribute("jid");
+ try {
+ sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid());
+ } catch (final InvalidJidException ignored) {
+ // TODO: Handle the case where an external JID is technically invalid?
+ }
+ }
+ }
}
});
}
@@ -854,14 +867,14 @@ public class XmppConnection implements Runnable {
}
private void processStreamError(Tag currentTag)
- throws XmlPullParserException, IOException {
+ throws XmlPullParserException, IOException {
Element streamError = tagReader.readElement(currentTag);
if (streamError != null && streamError.hasChild("conflict")) {
final String resource = account.getResource().split("\\.")[0];
- account.setResource(resource + "." + nextRandomId());
- Log.d(Config.LOGTAG,
+ account.setResource(resource + "." + nextRandomId());
+ Log.d(Config.LOGTAG,
account.getJid().toBareJid() + ": switching resource due to conflict ("
- + account.getResource() + ")");
+ + account.getResource() + ")");
}
}
@@ -906,11 +919,11 @@ public class XmppConnection implements Runnable {
}
private synchronized void sendPacket(final AbstractStanza packet,
- PacketReceived callback) {
+ PacketReceived callback) {
if (packet.getName().equals("iq") || packet.getName().equals("message")
|| packet.getName().equals("presence")) {
++stanzasSent;
- }
+ }
tagWriter.writeStanzaAsync(packet);
if (packet instanceof MessagePacket && packet.getId() != null
&& this.streamId != null) {
@@ -918,7 +931,7 @@ public class XmppConnection implements Runnable {
+ stanzasSent);
this.messageReceipts.put(stanzasSent, packet.getId());
tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
- }
+ }
if (callback != null) {
if (packet.getId() == null) {
packet.setId(nextRandomId());
@@ -942,22 +955,22 @@ public class XmppConnection implements Runnable {
public void setOnMessagePacketReceivedListener(
OnMessagePacketReceived listener) {
this.messageListener = listener;
- }
+ }
public void setOnUnregisteredIqPacketReceivedListener(
OnIqPacketReceived listener) {
this.unregisteredIqListener = listener;
- }
+ }
public void setOnPresencePacketReceivedListener(
OnPresencePacketReceived listener) {
this.presenceListener = listener;
- }
+ }
public void setOnJinglePacketReceivedListener(
OnJinglePacketReceived listener) {
this.jingleListener = listener;
- }
+ }
public void setOnStatusChangedListener(OnStatusChanged listener) {
this.statusListener = listener;
@@ -1083,9 +1096,9 @@ public class XmppConnection implements Runnable {
}
private boolean hasDiscoFeature(final Jid server, final String feature) {
- return connection.disco.containsKey(server.toDomainJid().toString()) &&
- connection.disco.get(server.toDomainJid().toString()).contains(feature);
- }
+ return connection.disco.containsKey(server.toDomainJid().toString()) &&
+ connection.disco.get(server.toDomainJid().toString()).contains(feature);
+ }
public boolean carbons() {
return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
@@ -1096,7 +1109,7 @@ public class XmppConnection implements Runnable {
}
public boolean csi() {
- return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
}
public boolean pubsub() {
@@ -1109,12 +1122,12 @@ public class XmppConnection implements Runnable {
}
public boolean rosterVersioning() {
- return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
}
public boolean streamhost() {
return connection
- .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
+ .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
}
public boolean compression() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
index 3ad3015d..ebf8a6ed 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
@@ -108,15 +108,16 @@ public final class Jid {
if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
}
- dp = jid.substring(domainpartStart, slashLoc);
+ dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES);
finaljid = finaljid + dp + "/" + rp;
} else {
resourcepart = "";
- dp = jid.substring(domainpartStart, jid.length());
+ dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()),
+ IDN.USE_STD3_ASCII_RULES);
finaljid = finaljid + dp;
}
- // Remove trailling "." before storing the domain part.
+ // Remove trailing "." before storing the domain part.
if (dp.endsWith(".")) {
try {
domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
index ca190deb..6ec79a82 100644
--- a/src/main/res/values-de/strings.xml
+++ b/src/main/res/values-de/strings.xml
@@ -108,6 +108,7 @@
<string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string>
<string name="pref_confirm_messages">Lesebestätigung senden</string>
<string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfängst oder liest</string>
+ <string name="pref_ui_options">Benutzeroberfläche</string>
<string name="openpgp_error">Fehler mit OpenKeychain</string>
<string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string>
<string name="accept">Annehmen</string>
@@ -231,9 +232,6 @@
<string name="server_info_session_established">Aktuelle Sitzung wiederhergestellt</string>
<string name="additional_information">Zusätzliche Informationen</string>
<string name="skip">Überspringen</string>
- <string name="pref_ui_options">Benutzeroberfläche</string>
- <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
- <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string>
<string name="disable_notifications">Benachrichtigungen deaktivieren</string>
<string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
<string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string>
@@ -256,13 +254,17 @@
<string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string>
<string name="pref_expert_options">Einstellungen für Experten</string>
<string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string>
+ <string name="title_activity_about">Über Conversations</string>
+ <string name="pref_about_conversations_summary">Versions- und Lizenzinformationen</string>
<string name="pref_use_larger_font">Schrift vergrößern</string>
<string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string>
<string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string>
<string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string>
+ <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
+ <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häkchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string>
<string name="pref_expert_options_other">Sonstiges</string>
<string name="pref_conference_name">Konferenz-Name</string>
- <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Name verwenden</string>
+ <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Namen verwenden</string>
<string name="toast_message_otr_fingerprint">OTR Fingerabdruck in die Zwischenablage kopiert!</string>
<string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string>
<string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string>
@@ -281,5 +283,8 @@
<string name="message_text">Nachrichtentext</string>
<string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string>
<string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string>
-
+ <string name="image_transmission_failed">Bild-Übertragung fehlgeschlagen</string>
+ <string name="scan_qr_code">Scanne QR-Code</string>
+ <string name="show_qr_code">Zeige QR-Code</string>
+ <string name="account_details">Account Details</string>
</resources>
diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml
index 0bb0e05e..3b613e9d 100644
--- a/src/main/res/values-it/strings.xml
+++ b/src/main/res/values-it/strings.xml
@@ -81,9 +81,9 @@
<string name="offering">offrendo&#8230;</string>
<string name="waiting">in attesa&#8230;</string>
<string name="no_pgp_key">Nessuna chiave OpenPGP trovata</string>
- <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perchè il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string>
+ <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perché il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string>
<string name="no_pgp_keys">Nessuna chiave OpenPGP trovata</string>
- <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perchè i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string>
+ <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perché i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string>
<string name="encrypted_message_received"><i>Messaggio cifrato ricevuto. Tocca per decifrare.</i></string>
<string name="encrypted_image_received"><i>Immagine cifrata ricevuta. Tocca per decifrare e mostrare.</i></string>
<string name="image_file"><i>Immagine ricevuta. Tocca per mostrare</i></string>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 8c4db54d..645dc8e5 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -256,7 +256,7 @@
<string name="pref_expert_options_summary">Please be careful with these</string>
<string name="title_activity_about">About Conversations</string>
<string name="pref_about_conversations_summary">Build and licensing information</string>
- <string name="pref_about_message">
+ <string name="pref_about_message" translatable="false">
Conversations • the very last word in instant messaging.
\n\nCopyright © 2014 Daniel Gultsch
\n\nThis program is free software: you can redistribute it and/or modify