diff options
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…</string> <string name="waiting">in attesa…</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 |