aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu
diff options
context:
space:
mode:
authorChristian S <christian@pix-art.de>2015-10-16 21:48:33 +0200
committerChristian S <christian@pix-art.de>2015-10-16 21:48:33 +0200
commit662b9e60da4605d954c50d80f12e348723b5c61e (patch)
tree1588b44c16ef0aa7ffe26be9f93feb19b91bf906 /src/main/java/eu
parent98c15fbc50f5f08a569355742e4d22b739151405 (diff)
parentc1716a35e359cf9b2e8d1b75cc4f0bac413bee5b (diff)
Merge remote-tracking branch 'siacs/master' into development
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java4
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java127
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java114
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java1
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/External.java29
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java2
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java3
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java16
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java28
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java16
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java31
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java2
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java6
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java40
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java185
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java3
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java195
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java45
-rw-r--r--src/main/java/eu/siacs/conversations/ui/UiCallback.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java14
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java7
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java2
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java19
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java197
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java4
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java3
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java4
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java2
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java3
29 files changed, 911 insertions, 197 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 5468819d3..45be9e1b0 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -49,6 +49,10 @@ public final class Config {
public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
+ public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
+
+ public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
+
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500;
diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
new file mode 100644
index 000000000..1fca865ed
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
@@ -0,0 +1,127 @@
+package eu.siacs.conversations.crypto;
+
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.DERTaggedObject;
+import org.bouncycastle.asn1.DERUTF8String;
+import org.bouncycastle.asn1.DLSequence;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public class XmppDomainVerifier implements HostnameVerifier {
+
+ private static final String LOGTAG = "XmppDomainVerifier";
+
+ private final String SRVName = "1.3.6.1.5.5.7.8.7";
+ private final String xmppAddr = "1.3.6.1.5.5.7.8.5";
+
+ @Override
+ public boolean verify(String domain, SSLSession sslSession) {
+ try {
+ Certificate[] chain = sslSession.getPeerCertificates();
+ if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
+ return false;
+ }
+ X509Certificate certificate = (X509Certificate) chain[0];
+ Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
+ List<String> xmppAddrs = new ArrayList<>();
+ List<String> srvNames = new ArrayList<>();
+ List<String> domains = new ArrayList<>();
+ if (alternativeNames != null) {
+ for (List<?> san : alternativeNames) {
+ Integer type = (Integer) san.get(0);
+ if (type == 0) {
+ Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
+ if (otherName != null) {
+ switch (otherName.first) {
+ case SRVName:
+ srvNames.add(otherName.second);
+ break;
+ case xmppAddr:
+ xmppAddrs.add(otherName.second);
+ break;
+ default:
+ Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
+ }
+ }
+ } else if (type == 2) {
+ Object value = san.get(1);
+ if (value instanceof String) {
+ domains.add((String) value);
+ }
+ }
+ }
+ }
+ if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
+ X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
+ RDN[] rdns = x500name.getRDNs(BCStyle.CN);
+ for (int i = 0; i < rdns.length; ++i) {
+ domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue()));
+ }
+ }
+ Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + srvNames + " xmppAddrs: " + xmppAddrs + " domains:" + domains);
+ return xmppAddrs.contains(domain) || srvNames.contains("_xmpp-client." + domain) || matchDomain(domain, domains);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static Pair<String, String> parseOtherName(byte[] otherName) {
+ try {
+ ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName);
+ if (asn1Primitive instanceof DERTaggedObject) {
+ ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject();
+ if (inner instanceof DLSequence) {
+ DLSequence sequence = (DLSequence) inner;
+ if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) {
+ String oid = sequence.getObjectAt(0).toString();
+ ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject();
+ if (value instanceof DERUTF8String) {
+ return new Pair<>(oid, ((DERUTF8String) value).getString());
+ } else if (value instanceof DERIA5String) {
+ return new Pair<>(oid, ((DERIA5String) value).getString());
+ }
+ }
+ }
+ }
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static boolean matchDomain(String needle, List<String> haystack) {
+ for (String entry : haystack) {
+ if (entry.startsWith("*.")) {
+ int i = needle.indexOf('.');
+ Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
+ if (i != -1 && needle.substring(i).equals(entry.substring(1))) {
+ Log.d(LOGTAG, "domain " + needle + " matched " + entry);
+ return true;
+ }
+ } else {
+ if (entry.equals(needle)) {
+ Log.d(LOGTAG, "domain " + needle + " matched " + entry);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
index 70de2777d..58e5a0957 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.crypto.axolotl;
+import android.security.KeyChain;
+import android.security.KeyChainException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -18,7 +20,12 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
import java.security.Security;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -46,6 +53,7 @@ public class AxolotlService {
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
+ public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification";
public static final String LOGPREFIX = "AxolotlService";
@@ -140,9 +148,6 @@ public class AxolotlService {
putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
for (Contact contact : account.getRoster().getContacts()) {
Jid bareJid = contact.getJid().toBareJid();
- if (bareJid == null) {
- continue; // FIXME: handle this?
- }
String address = bareJid.toString();
deviceIds = store.getSubDeviceSessions(address);
putDevicesForJid(address, deviceIds, store);
@@ -162,7 +167,7 @@ public class AxolotlService {
}
}
- private static enum FetchStatus {
+ private enum FetchStatus {
PENDING,
SUCCESS,
ERROR
@@ -212,14 +217,12 @@ public class AxolotlService {
private Set<XmppAxolotlSession> findOwnSessions() {
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
- Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
- return ownDeviceSessions;
+ return new HashSet<>(this.sessions.getAll(ownAddress).values());
}
private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
- Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
- return sessions;
+ return new HashSet<>(this.sessions.getAll(contactAddress).values());
}
public Set<String> getFingerprintsForOwnSessions() {
@@ -247,11 +250,11 @@ public class AxolotlService {
return this.pepBroken;
}
- public void regenerateKeys() {
+ public void regenerateKeys(boolean wipeOther) {
axolotlStore.regenerate();
sessions.clear();
fetchStatusMap.clear();
- publishBundlesIfNeeded(true);
+ publishBundlesIfNeeded(true, wipeOther);
}
public int getOwnDeviceId() {
@@ -385,7 +388,33 @@ public class AxolotlService {
}
}
- public void publishBundlesIfNeeded(final boolean announceAfter) {
+ public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
+ final Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
+ try {
+ IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
+ PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
+ X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
+ Signature verifier = Signature.getInstance("sha256WithRSA");
+ verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG());
+ verifier.update(axolotlPublicKey.serialize());
+ byte[] signature = verifier.sign();
+ IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
+ Log.d(Config.LOGTAG,"verification : "+packet.toString());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
+ }
+ });
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
if (pepBroken) {
Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
return;
@@ -475,44 +504,56 @@ public class AxolotlService {
if (changed) {
- IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
- signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
- preKeyRecords, getOwnDeviceId());
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
- mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
- @Override
- public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
- if (announceAfter) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
- publishOwnDeviceIdIfNeeded();
- }
- } else {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
- }
- }
- });
+ if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
+ publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
+ } else {
+ publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
+ }
} else {
Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
- if (announceAfter) {
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announce) {
Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
publishOwnDeviceIdIfNeeded();
}
}
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
- return;
}
}
});
}
- public boolean isContactAxolotlCapable(Contact contact) {
+ private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
+ Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
+ signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
+ preKeyRecords, getOwnDeviceId());
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announceAfter) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
+ publishOwnDeviceIdIfNeeded();
+ }
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
+ }
+ }
+ });
+ }
+ public boolean isContactAxolotlCapable(Contact contact) {
Jid jid = contact.getJid().toBareJid();
- AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
- return sessions.hasAny(address) ||
+ return hasAny(contact) ||
(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
}
@@ -587,7 +628,6 @@ public class AxolotlService {
fetchStatusMap.put(address, FetchStatus.ERROR);
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
finish();
- return;
}
}
});
@@ -781,7 +821,7 @@ public class AxolotlService {
plaintextMessage = message.decrypt(session, getOwnDeviceId());
Integer preKeyId = session.getPreKeyId();
if (preKeyId != null) {
- publishBundlesIfNeeded(false);
+ publishBundlesIfNeeded(false, false);
session.resetPreKeyId();
}
} catch (CryptoFailedException e) {
@@ -796,7 +836,7 @@ public class AxolotlService {
}
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
- XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
XmppAxolotlSession session = getReceivingSession(message);
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
index 4d7793023..a78317183 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
@@ -88,7 +88,6 @@ public class SQLiteAxolotlStore implements AxolotlStore {
// --------------------------------------
private IdentityKeyPair loadIdentityKeyPair() {
- String ownName = account.getJid().toBareJid().toString();
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
if (ownKey != null) {
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java
new file mode 100644
index 000000000..df92898c1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java
@@ -0,0 +1,29 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class External extends SaslMechanism {
+
+ public External(TagWriter tagWriter, Account account, SecureRandom rng) {
+ super(tagWriter, account, rng);
+ }
+
+ @Override
+ public int getPriority() {
+ return 25;
+ }
+
+ @Override
+ public String getMechanism() {
+ return "EXTERNAL";
+ }
+
+ @Override
+ public String getClientFirstMessage() {
+ return Base64.encodeToString(account.getJid().toBareJid().toString().getBytes(),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
index 14d8b944b..5b4b99efb 100644
--- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
@@ -11,7 +11,7 @@ public abstract class SaslMechanism {
final protected Account account;
final protected SecureRandom rng;
- protected static enum State {
+ protected enum State {
INITIAL,
AUTH_TEXT_SENT,
RESPONSE_SENT,
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
index f47677f6e..3a05446c1 100644
--- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
@@ -21,7 +21,6 @@ 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;
@@ -104,7 +103,7 @@ public class ScramSha1 extends SaslMechanism {
if (challenge == null) {
throw new AuthenticationException("challenge can not be null");
}
- serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
+ byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
int iterationCount = -1;
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index 002c276e2..0eb38c9fd 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -194,18 +194,14 @@ public class Account extends AbstractEntity {
return jid.getLocalpart();
}
- public void setUsername(final String username) throws InvalidJidException {
- jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart());
+ public void setJid(final Jid jid) {
+ this.jid = jid;
}
public Jid getServer() {
return jid.toDomainJid();
}
- public void setServer(final String server) throws InvalidJidException {
- jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart());
- }
-
public String getPassword() {
return password;
}
@@ -272,6 +268,14 @@ public class Account extends AbstractEntity {
}
}
+ public boolean setPrivateKeyAlias(String alias) {
+ return setKey("private_key_alias", alias);
+ }
+
+ public String getPrivateKeyAlias() {
+ return getKey("private_key_alias");
+ }
+
@Override
public ContentValues getContentValues() {
final ContentValues values = new ContentValues();
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 8ffc44141..c9cfb9b35 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -112,6 +112,16 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
+ public void findUnreadMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (!message.isRead()) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (final Message message : this.messages) {
@@ -267,9 +277,8 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
-
public interface OnMessageFound {
- public void onMessageFound(final Message message);
+ void onMessageFound(final Message message);
}
public Conversation(final String name, final Account account, final Jid contactJid,
@@ -302,13 +311,18 @@ public class Conversation extends AbstractEntity implements Blockable {
return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
}
- public void markRead() {
- for (int i = this.messages.size() - 1; i >= 0; --i) {
- if (messages.get(i).isRead()) {
- break;
+ public List<Message> markRead() {
+ final List<Message> unread = new ArrayList<>();
+ synchronized (this.messages) {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).isRead()) {
+ break;
+ }
+ this.messages.get(i).markRead();
+ unread.add(this.messages.get(i));
}
- this.messages.get(i).markRead();
}
+ return unread;
}
public Message getLatestMarkableMessage() {
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index bfb26446d..36cc0842f 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -56,6 +56,7 @@ public class Message extends AbstractEntity {
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
+ public static final String READ = "read";
public static final String ME_COMMAND = "/me ";
@@ -87,11 +88,8 @@ public class Message extends AbstractEntity {
public Message(Conversation conversation, String body, int encryption) {
this(conversation, body, encryption, STATUS_UNSEND);
}
- public Message(Conversation conversation, String body, int encryption, int status) {
- this(conversation, body, encryption, status, false);
- }
- public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) {
+ public Message(Conversation conversation, String body, int encryption, int status) {
this(java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
@@ -105,7 +103,8 @@ public class Message extends AbstractEntity {
null,
null,
null,
- null);
+ null,
+ true);
this.conversation = conversation;
}
@@ -113,7 +112,7 @@ public class Message extends AbstractEntity {
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath,
- final String serverMsgId, final String fingerprint) {
+ final String serverMsgId, final String fingerprint, final boolean read) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@@ -128,6 +127,7 @@ public class Message extends AbstractEntity {
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
+ this.read = read;
}
public static Message fromCursor(Cursor cursor) {
@@ -166,7 +166,8 @@ public class Message extends AbstractEntity {
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
- cursor.getString(cursor.getColumnIndex(FINGERPRINT)));
+ cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
+ cursor.getInt(cursor.getColumnIndex(READ)) > 0);
}
public static Message createStatusMessage(Conversation conversation, String body) {
@@ -202,6 +203,7 @@ public class Message extends AbstractEntity {
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
+ values.put(READ,read);
return values;
}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index 42c57b24f..fb69860db 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -2,16 +2,20 @@ package eu.siacs.conversations.generator;
import android.util.Base64;
+import android.util.Log;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
@@ -173,6 +177,23 @@ public class IqGenerator extends AbstractGenerator {
return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
}
+ public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
+ final Element item = new Element("item");
+ final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
+ final Element chain = verification.addChild("chain");
+ for(int i = 0; i < certificates.length; ++i) {
+ try {
+ Element certificate = chain.addChild("certificate");
+ certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT));
+ certificate.setAttribute("index",i);
+ } catch (CertificateEncodingException e) {
+ Log.d(Config.LOGTAG, "could not encode certificate");
+ }
+ }
+ verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT));
+ return publish(AxolotlService.PEP_VERIFICATION+":"+deviceId, item);
+ }
+
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element query = packet.query("urn:xmpp:mam:0");
@@ -266,4 +287,14 @@ public class IqGenerator extends AbstractGenerator {
}
return packet;
}
+
+ public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
+
+ register.setTo(account.getServer());
+ register.setId(id);
+ register.query("jabber:iq:register").addChild(data);
+
+ return register;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 4d0577566..c53e50ac4 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -315,7 +315,7 @@ public class MessageParser extends AbstractParser implements
status = Message.STATUS_SEND_RECEIVED;
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status)) {
return;
- } else if (remoteMsgId == null) {
+ } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
Message message = conversation.findSentMessageWithBody(packet.getBody());
if (message != null) {
mXmppConnectionService.markMessage(message, status);
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index be827929c..070713236 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -43,7 +43,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 17;
+ private static final int DATABASE_VERSION = 18;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -143,6 +143,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, "
+ + Message.READ + " NUMBER DEFAULT 1, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
@@ -320,6 +321,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
}
+ if (oldVersion < 18 && newVersion >= 18) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "+ Message.READ+ " NUMBER DEFAULT 1");
+ }
}
public static synchronized DatabaseBackend getInstance(Context context) {
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index b7c872d48..515e57c85 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -1,6 +1,5 @@
package eu.siacs.conversations.services;
-import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -10,7 +9,6 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
-import android.os.PowerManager;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigPictureStyle;
@@ -114,28 +112,42 @@ public class NotificationService {
public boolean conferenceNotificationsEnabled() {
return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false);
}
-
+
+ public void pushFromBacklog(final Message message) {
+ if (notify(message)) {
+ pushToStack(message);
+ }
+ }
+
+ public void finishBacklog() {
+ synchronized (notifications) {
+ mXmppConnectionService.updateUnreadCountBadge();
+ updateNotification(false);
+ }
+ }
+
+ private void pushToStack(final Message message) {
+ final String conversationUuid = message.getConversationUuid();
+ if (notifications.containsKey(conversationUuid)) {
+ notifications.get(conversationUuid).add(message);
+ } else {
+ final ArrayList<Message> mList = new ArrayList<>();
+ mList.add(message);
+ notifications.put(conversationUuid, mList);
+ }
+ }
+
public void push(final Message message) {
mXmppConnectionService.updateUnreadCountBadge();
if (!notify(message)) {
return;
}
-
final boolean isScreenOn = mXmppConnectionService.isInteractive();
-
if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
return;
}
-
synchronized (notifications) {
- final String conversationUuid = message.getConversationUuid();
- if (notifications.containsKey(conversationUuid)) {
- notifications.get(conversationUuid).add(message);
- } else {
- final ArrayList<Message> mList = new ArrayList<>();
- mList.add(message);
- notifications.put(conversationUuid, mList);
- }
+ pushToStack(message);
final Account account = message.getConversation().getAccount();
final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
&& !account.inGracePeriod()
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index ddc40d34e..f9f9b71e2 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -25,8 +25,12 @@ import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
+import android.security.KeyChain;
+import android.security.KeyChainException;
import android.util.Log;
import android.util.LruCache;
+import android.util.DisplayMetrics;
+import android.util.Pair;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
@@ -34,11 +38,18 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.math.BigInteger;
import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -161,7 +172,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
mMessageArchiveService.executePendingQueries(account);
mJingleConnectionManager.cancelInTransmission();
syncDirtyContacts(account);
- account.getAxolotlService().publishBundlesIfNeeded(true);
+ account.getAxolotlService().publishBundlesIfNeeded(true, false);
}
};
private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
@@ -244,6 +255,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int showErrorToastListenerCount = 0;
private int unreadCount = -1;
private OnAccountUpdate mOnAccountUpdate = null;
+ private OnCaptchaRequested mOnCaptchaRequested = null;
private OnStatusChanged statusListener = new OnStatusChanged() {
@Override
@@ -302,6 +314,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
};
private int accountChangedListenerCount = 0;
+ private int captchaRequestedListenerCount = 0;
private OnRosterUpdate mOnRosterUpdate = null;
private OnUpdateBlocklist mOnUpdateBlocklist = null;
private int updateBlocklistListenerCount = 0;
@@ -661,6 +674,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
@Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ if (level >= TRIM_MEMORY_COMPLETE) {
+ Log.d(Config.LOGTAG,"clear cache due to low memory");
+ getBitmapCache().evictAll();
+ }
+ }
+
+ @Override
public void onDestroy() {
try {
unregisterReceiver(this.mEventReceiver);
@@ -1059,7 +1081,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
for (Conversation conversation : conversations) {
conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
checkDeletedFiles(conversation);
+ conversation.findUnreadMessages(new Conversation.OnMessageFound() {
+ @Override
+ public void onMessageFound(Message message) {
+ mNotificationService.pushFromBacklog(message);
+ }
+ });
}
+ mNotificationService.finishBacklog();
mRestoredFromDatabase = true;
Log.d(Config.LOGTAG,"restored all messages");
updateConversationUi();
@@ -1285,6 +1314,67 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
updateAccountUi();
}
+ public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
+ Pair<Jid,String> info = CryptoHelper.extractJidAndName(chain[0]);
+ if (findAccountByJid(info.first) == null) {
+ Account account = new Account(info.first, "");
+ account.setPrivateKeyAlias(alias);
+ account.setOption(Account.OPTION_DISABLED, true);
+ createAccount(account);
+ callback.onAccountCreated(account);
+ if (Config.X509_VERIFICATION) {
+ try {
+ getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ } catch (CertificateException e) {
+ callback.informUser(R.string.certificate_chain_is_not_trusted);
+ }
+ }
+ } else {
+ callback.informUser(R.string.account_already_exists);
+ }
+ } catch (Exception e) {
+ callback.informUser(R.string.unable_to_parse_certificate);
+ }
+ }
+ }).start();
+
+ }
+
+ public void updateKeyInAccount(final Account account, final String alias) {
+ Log.d(Config.LOGTAG, "update key in account " + alias);
+ try {
+ X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
+ Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
+ if (account.getJid().toBareJid().equals(info.first)) {
+ account.setPrivateKeyAlias(alias);
+ databaseBackend.updateAccount(account);
+ if (Config.X509_VERIFICATION) {
+ try {
+ getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ } catch (CertificateException e) {
+ showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
+ }
+ account.getAxolotlService().regenerateKeys(true);
+ }
+ } else {
+ showErrorToastInUi(R.string.jid_does_not_match_certificate);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (KeyChainException e) {
+ e.printStackTrace();
+ } catch (InvalidJidException e) {
+ e.printStackTrace();
+ } catch (CertificateEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
public void updateAccount(final Account account) {
this.statusListener.onStatusChanged(account);
databaseBackend.updateAccount(account);
@@ -1409,6 +1499,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnCaptchaRequested = listener;
+ if (this.captchaRequestedListenerCount < 2) {
+ this.captchaRequestedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnCaptchaRequestedListener() {
+ synchronized (this) {
+ this.captchaRequestedListenerCount--;
+ if (this.captchaRequestedListenerCount <= 0) {
+ this.mOnCaptchaRequested = null;
+ this.captchaRequestedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
synchronized (this) {
if (checkListeners()) {
@@ -1513,6 +1628,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
return (this.mOnAccountUpdate == null
&& this.mOnConversationUpdate == null
&& this.mOnRosterUpdate == null
+ && this.mOnCaptchaRequested == null
&& this.mOnUpdateBlocklist == null
&& this.mOnShowErrorToast == null
&& this.mOnKeyStatusUpdated == null);
@@ -2414,6 +2530,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
+ boolean rc = false;
+ if (mOnCaptchaRequested != null) {
+ DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
+ Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int)(captcha.getWidth() * metrics.scaledDensity),
+ (int)(captcha.getHeight() * metrics.scaledDensity), false);
+
+ mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
+ rc = true;
+ }
+
+ return rc;
+ }
+
public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
if (mOnUpdateBlocklist != null) {
mOnUpdateBlocklist.OnUpdateBlocklist(status);
@@ -2452,7 +2582,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void markRead(final Conversation conversation) {
mNotificationService.clear(conversation);
- conversation.markRead();
+ for(Message message : conversation.markRead()) {
+ databaseBackend.updateMessage(message);
+ }
updateUnreadCountBadge();
}
@@ -2570,6 +2702,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendCaptchaRegistryRequest(id, data);
+ }
+ }
+
public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
final XmppConnection connection = account.getXmppConnection();
if (connection != null) {
@@ -2699,54 +2838,66 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public interface OnAccountCreated {
+ void onAccountCreated(Account account);
+ void informUser(int r);
+ }
+
public interface OnMoreMessagesLoaded {
- public void onMoreMessagesLoaded(int count, Conversation conversation);
+ void onMoreMessagesLoaded(int count, Conversation conversation);
- public void informUser(int r);
+ void informUser(int r);
}
public interface OnAccountPasswordChanged {
- public void onPasswordChangeSucceeded();
+ void onPasswordChangeSucceeded();
- public void onPasswordChangeFailed();
+ void onPasswordChangeFailed();
}
public interface OnAffiliationChanged {
- public void onAffiliationChangedSuccessful(Jid jid);
+ void onAffiliationChangedSuccessful(Jid jid);
- public void onAffiliationChangeFailed(Jid jid, int resId);
+ void onAffiliationChangeFailed(Jid jid, int resId);
}
public interface OnRoleChanged {
- public void onRoleChangedSuccessful(String nick);
+ void onRoleChangedSuccessful(String nick);
- public void onRoleChangeFailed(String nick, int resid);
+ void onRoleChangeFailed(String nick, int resid);
}
public interface OnConversationUpdate {
- public void onConversationUpdate();
+ void onConversationUpdate();
}
public interface OnAccountUpdate {
- public void onAccountUpdate();
+ void onAccountUpdate();
+ }
+
+ public interface OnCaptchaRequested {
+ void onCaptchaRequested(Account account,
+ String id,
+ Data data,
+ Bitmap captcha);
}
public interface OnRosterUpdate {
- public void onRosterUpdate();
+ void onRosterUpdate();
}
public interface OnMucRosterUpdate {
- public void onMucRosterUpdate();
+ void onMucRosterUpdate();
}
public interface OnConferenceConfigurationFetched {
- public void onConferenceConfigurationFetched(Conversation conversation);
+ void onConferenceConfigurationFetched(Conversation conversation);
}
public interface OnConferenceOptionsPushed {
- public void onPushSucceeded();
+ void onPushSucceeded();
- public void onPushFailed();
+ void onPushFailed();
}
public interface OnShowErrorToast {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
index 8783216b3..b3765a080 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -220,8 +220,7 @@ public class ConversationActivity extends XmppActivity
return null;
}
listAdapter.remove(swipedConversation);
- swipedConversation.markRead();
- xmppConnectionService.getNotificationService().clear(swipedConversation);
+ xmppConnectionService.markRead(swipedConversation);
final boolean formerlySelected = (getSelectedConversation() == swipedConversation);
if (position == 0 && listAdapter.getCount() == 0) {
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index dd5111b1b..2ab60800d 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -1,10 +1,14 @@
package eu.siacs.conversations.ui;
+import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
+import android.graphics.Bitmap;
import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
@@ -31,17 +35,21 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
+import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
+import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
-public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated {
+public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
+ OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast {
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
@@ -72,8 +80,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys;
private LinearLayout keysCard;
+ private AlertDialog mCaptchaDialog = null;
private Jid jidToEdit;
+ private boolean mInitMode = false;
private Account mAccount;
private String messageFingerprint;
@@ -83,6 +93,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
+ if (mInitMode && mAccount != null) {
+ mAccount.setOption(Account.OPTION_DISABLED, false);
+ }
if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited()) {
mAccount.setOption(Account.OPTION_DISABLED, false);
xmppConnectionService.updateAccount(mAccount);
@@ -97,7 +110,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final Jid jid;
try {
if (Config.DOMAIN_LOCK != null) {
- jid = Jid.fromParts(mAccountJid.getText().toString(),Config.DOMAIN_LOCK,null);
+ jid = Jid.fromParts(mAccountJid.getText().toString(), Config.DOMAIN_LOCK, null);
} else {
jid = Jid.fromString(mAccountJid.getText().toString());
}
@@ -129,12 +142,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
}
if (mAccount != null) {
- try {
- mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : "");
- mAccount.setServer(jid.getDomainpart());
- } catch (final InvalidJidException ignored) {
- return;
- }
+ mAccount.setJid(jid);
mAccountJid.setError(null);
mPasswordConfirm.setError(null);
mAccount.setPassword(password);
@@ -152,9 +160,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
xmppConnectionService.createAccount(mAccount);
}
- if (jidToEdit != null
- && !mAccount.isOptionSet(Account.OPTION_DISABLED)
- && !registerNewAccount) {
+ if (!mAccount.isOptionSet(Account.OPTION_DISABLED)
+ && !registerNewAccount
+ && !mInitMode) {
finish();
} else {
updateSaveButton();
@@ -177,14 +185,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
&& mAccount.getStatus() != Account.State.ONLINE
&& mFetchingAvatar) {
startActivity(new Intent(getApplicationContext(),
- ManageAccountActivity.class));
+ ManageAccountActivity.class));
finish();
- } else if (jidToEdit == null && mAccount != null
- && mAccount.getStatus() == Account.State.ONLINE) {
+ } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
if (!mFetchingAvatar) {
mFetchingAvatar = true;
- xmppConnectionService.checkForAvatar(mAccount,
- mAvatarFetchCallback);
+ xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
}
} else {
updateSaveButton();
@@ -198,6 +204,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
public void onAccountUpdate() {
refreshUi();
}
+
private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
@Override
@@ -236,8 +243,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View view) {
if (mAccount != null) {
- final Intent intent = new Intent(getApplicationContext(),
- PublishProfilePictureActivity.class);
+ final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
intent.putExtra("account", mAccount.getJid().toBareJid().toString());
startActivity(intent);
}
@@ -269,7 +275,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
protected void updateSaveButton() {
- if (accountInfoEdited() && jidToEdit != null) {
+ if (accountInfoEdited() && !mInitMode) {
this.mSaveButton.setText(R.string.save);
this.mSaveButton.setEnabled(true);
this.mSaveButton.setTextColor(getPrimaryTextColor());
@@ -277,14 +283,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mSaveButton.setEnabled(false);
this.mSaveButton.setTextColor(getSecondaryTextColor());
this.mSaveButton.setText(R.string.account_status_connecting);
- } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) {
+ } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
this.mSaveButton.setEnabled(true);
this.mSaveButton.setTextColor(getPrimaryTextColor());
this.mSaveButton.setText(R.string.enable);
} else {
this.mSaveButton.setEnabled(true);
this.mSaveButton.setTextColor(getPrimaryTextColor());
- if (jidToEdit != null) {
+ if (!mInitMode) {
if (mAccount != null && mAccount.isOnlineAndConnected()) {
this.mSaveButton.setText(R.string.save);
if (!accountInfoEdited()) {
@@ -316,7 +322,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
protected String getShareableUri() {
- if (mAccount!=null) {
+ if (mAccount != null) {
return mAccount.getShareableUri();
} else {
return "";
@@ -367,7 +373,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(final CompoundButton buttonView,
- final boolean isChecked) {
+ final boolean isChecked) {
if (isChecked) {
mPasswordConfirm.setVisibility(View.VISIBLE);
} else {
@@ -391,6 +397,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
+ final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
+
+ renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
+
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
@@ -421,8 +431,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} catch (final InvalidJidException | NullPointerException ignored) {
this.jidToEdit = null;
}
+ this.mInitMode = getIntent().getBooleanExtra("init", false) || this.jidToEdit == null;
this.messageFingerprint = getIntent().getStringExtra("fingerprint");
- if (this.jidToEdit != null) {
+ if (!mInitMode) {
this.mRegisterNew.setVisibility(View.GONE);
if (getActionBar() != null) {
getActionBar().setTitle(getString(R.string.account_details));
@@ -440,7 +451,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
protected void onBackendConnected() {
if (this.jidToEdit != null) {
this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
- updateAccountInformation(true);
+ if (this.mAccount != null) {
+ if (this.mAccount.getPrivateKeyAlias() != null) {
+ this.mPassword.setHint(R.string.authenticate_with_certificate);
+ if (this.mInitMode) {
+ this.mPassword.requestFocus();
+ }
+ }
+ updateAccountInformation(true);
+ }
} else if (this.xmppConnectionService.getAccounts().size() == 0) {
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(false);
@@ -479,20 +498,35 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
case R.id.action_clear_devices:
showWipePepDialog();
break;
+ case R.id.action_renew_certificate:
+ renewCertificate();
+ break;
}
return super.onOptionsItemSelected(item);
}
+ private void renewCertificate() {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ }
+
+ @Override
+ public void alias(String alias) {
+ if (alias != null) {
+ xmppConnectionService.updateKeyInAccount(mAccount, alias);
+ }
+ }
+
private void updateAccountInformation(boolean init) {
if (init) {
+ this.mAccountJid.getEditableText().clear();
if (Config.DOMAIN_LOCK != null) {
- this.mAccountJid.setText(this.mAccount.getJid().getLocalpart());
+ this.mAccountJid.getEditableText().append(this.mAccount.getJid().getLocalpart());
} else {
- this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString());
+ this.mAccountJid.getEditableText().append(this.mAccount.getJid().toBareJid().toString());
}
this.mPassword.setText(this.mAccount.getPassword());
}
- if (this.jidToEdit != null) {
+ if (!mInitMode) {
this.mAvatar.setVisibility(View.VISIBLE);
this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(Config.AVATAR_SIZE)));
}
@@ -507,7 +541,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
this.mStats.setVisibility(View.VISIBLE);
this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
- .getLastSessionEstablished()));
+ .getLastSessionEstablished()));
Features features = this.mAccount.getXmppConnection().getFeatures();
if (features.rosterVersioning()) {
this.mServerInfoRosterVersion.setText(R.string.server_info_available);
@@ -518,7 +552,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mServerInfoCarbons.setText(R.string.server_info_available);
} else {
this.mServerInfoCarbons
- .setText(R.string.server_info_unavailable);
+ .setText(R.string.server_info_unavailable);
}
if (features.mam()) {
this.mServerInfoMam.setText(R.string.server_info_available);
@@ -560,21 +594,21 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.mOtrFingerprintToClipboardButton
- .setVisibility(View.VISIBLE);
+ .setVisibility(View.VISIBLE);
this.mOtrFingerprintToClipboardButton
- .setOnClickListener(new View.OnClickListener() {
+ .setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(final View v) {
+ @Override
+ public void onClick(final View v) {
- if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
- Toast.makeText(
- EditAccountActivity.this,
- R.string.toast_message_otr_fingerprint,
- Toast.LENGTH_SHORT).show();
+ if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_otr_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
}
- }
- });
+ });
} else {
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
@@ -617,7 +651,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
boolean hasKeys = false;
keys.removeAllViews();
for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) {
- if(ownFingerprint.equals(fingerprint)) {
+ if (ownFingerprint.equals(fingerprint)) {
continue;
}
boolean highlight = fingerprint.equals(messageFingerprint);
@@ -651,7 +685,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- mAccount.getAxolotlService().regenerateKeys();
+ mAccount.getAxolotlService().regenerateKeys(false);
}
});
builder.create().show();
@@ -677,4 +711,79 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
public void onKeyStatusUpdated() {
refreshUi();
}
+
+ @Override
+ public void onCaptchaRequested(final Account account, final String id, final Data data,
+ final Bitmap captcha) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final ImageView view = new ImageView(this);
+ final LinearLayout layout = new LinearLayout(this);
+ final EditText input = new EditText(this);
+
+ view.setImageBitmap(captcha);
+ view.setScaleType(ImageView.ScaleType.FIT_CENTER);
+
+ input.setHint(getString(R.string.captcha_hint));
+
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.addView(view);
+ layout.addView(input);
+
+ builder.setTitle(getString(R.string.captcha_required));
+ builder.setView(layout);
+
+ builder.setPositiveButton(getString(R.string.ok),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String rc = input.getText().toString();
+ data.put("username", account.getUsername());
+ data.put("password", account.getPassword());
+ data.put("ocr", rc);
+ data.submit();
+
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(
+ account, id, data);
+ }
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+ }
+ }
+ });
+
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+ }
+ }
+ });
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if ((mCaptchaDialog != null) && mCaptchaDialog.isShowing()) {
+ mCaptchaDialog.dismiss();
+ }
+ mCaptchaDialog = builder.create();
+ mCaptchaDialog.show();
+ }
+ });
+ }
+
+ public void onShowErrorToast(final int resId) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
index 5b9b73550..6024177ae 100644
--- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -5,6 +5,8 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
@@ -14,6 +16,7 @@ import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
+import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
@@ -21,10 +24,11 @@ import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.AccountAdapter;
-public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {
+public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated {
protected Account selectedAccount = null;
@@ -61,7 +65,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
@Override
public void onItemClick(AdapterView<?> arg0, View view,
- int position, long arg3) {
+ int position, long arg3) {
switchToAccount(accountList.get(position));
}
});
@@ -98,6 +102,14 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.manageaccounts, menu);
MenuItem enableAll = menu.findItem(R.id.action_enable_all);
+ MenuItem addAccount = menu.findItem(R.id.action_add_account);
+ MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
+
+ if (Config.X509_VERIFICATION) {
+ addAccount.setVisible(false);
+ addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+
if (!accountsLeftToEnable()) {
enableAll.setVisible(false);
}
@@ -144,6 +156,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
case R.id.action_enable_all:
enableAllAccounts();
break;
+ case R.id.action_add_account_with_cert:
+ addAccountFromKey();
+ break;
default:
break;
}
@@ -179,6 +194,10 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
}
}
+ private void addAccountFromKey() {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ }
+
private void publishAvatar(Account account) {
Intent intent = new Intent(getApplicationContext(),
PublishProfilePictureActivity.class);
@@ -281,4 +300,26 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
}
}
}
+
+ @Override
+ public void alias(String alias) {
+ if (alias != null) {
+ xmppConnectionService.createAccountFromKey(alias, this);
+ }
+ }
+
+ @Override
+ public void onAccountCreated(Account account) {
+ switchToAccount(account, true);
+ }
+
+ @Override
+ public void informUser(final int r) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ManageAccountActivity.this,r,Toast.LENGTH_LONG).show();
+ }
+ });
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
index c80199e17..d056d6289 100644
--- a/src/main/java/eu/siacs/conversations/ui/UiCallback.java
+++ b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
@@ -3,9 +3,9 @@ package eu.siacs.conversations.ui;
import android.app.PendingIntent;
public interface UiCallback<T> {
- public void success(T object);
+ void success(T object);
- public void error(int errorCode, T object);
+ void error(int errorCode, T object);
- public void userInputRequried(PendingIntent pi, T object);
+ void userInputRequried(PendingIntent pi, T object);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index ed460740b..de4d261c5 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -281,6 +281,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
}
+ if (this instanceof XmppConnectionService.OnCaptchaRequested) {
+ this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
+ }
if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
}
@@ -305,6 +308,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.removeOnAccountListChangedListener();
}
+ if (this instanceof XmppConnectionService.OnCaptchaRequested) {
+ this.xmppConnectionService.removeOnCaptchaRequestedListener();
+ }
if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.removeOnRosterUpdateListener();
}
@@ -438,8 +444,13 @@ public abstract class XmppActivity extends Activity {
}
public void switchToAccount(Account account) {
+ switchToAccount(account,false);
+ }
+
+ public void switchToAccount(Account account, boolean init) {
Intent intent = new Intent(this, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().toBareJid().toString());
+ intent.putExtra("init", init);
startActivity(intent);
}
@@ -617,6 +628,9 @@ public abstract class XmppActivity extends Activity {
protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight) {
final XmppAxolotlSession.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint);
+ if (trust == null) {
+ return false;
+ }
return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
new CompoundButton.OnCheckedChangeListener() {
@Override
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
index 471526afe..39bfc082e 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
@@ -15,7 +15,7 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint != null) {
- ArrayList<String> suggestions = new ArrayList<String>();
+ ArrayList<String> suggestions = new ArrayList<>();
final String[] split = constraint.toString().split("@");
if (split.length == 1) {
for (String domain : domains) {
@@ -58,10 +58,9 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
}
};
- public KnownHostsAdapter(Context context, int viewResourceId,
- List<String> mKnownHosts) {
+ public KnownHostsAdapter(Context context, int viewResourceId, List<String> mKnownHosts) {
super(context, viewResourceId, new ArrayList<String>());
- domains = new ArrayList<String>(mKnownHosts);
+ domains = new ArrayList<>(mKnownHosts);
}
@Override
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
index ad7d76224..4be4931f7 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
@@ -92,7 +92,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
}
public interface OnTagClickedListener {
- public void onTagClicked(String tag);
+ void onTagClicked(String tag);
}
class BitmapWorkerTask extends AsyncTask<ListItem, Void, Bitmap> {
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
index c7c9ac423..e9ad71971 100644
--- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -1,6 +1,15 @@
package eu.siacs.conversations.utils;
+import android.util.Pair;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+
import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Collection;
@@ -9,6 +18,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
public final class CryptoHelper {
public static final String FILETRANSFER = "?FILETRANSFERv1:";
@@ -125,4 +136,12 @@ public final class CryptoHelper {
}
}
}
+
+ public static Pair<Jid,String> extractJidAndName(X509Certificate certificate) throws CertificateEncodingException, InvalidJidException {
+ X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
+ //String xmpp = IETFUtils.valueToString(x500name.getRDNs(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.8.5"))[0].getFirst().getValue());
+ String email = IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue());
+ String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
+ return new Pair<>(Jid.fromString(email),name);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index c41b69748..d21682a79 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -1,10 +1,14 @@
package eu.siacs.conversations.xmpp;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
+import android.security.KeyChain;
+import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -14,6 +18,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -24,8 +29,12 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
+import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -37,14 +46,18 @@ import java.util.List;
import java.util.Map.Entry;
import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.sasl.DigestMd5;
+import eu.siacs.conversations.crypto.sasl.External;
import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramSha1;
@@ -59,6 +72,8 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
@@ -116,6 +131,67 @@ public class XmppConnection implements Runnable {
private SaslMechanism saslMechanism;
+ private X509KeyManager mKeyManager = new X509KeyManager() {
+ @Override
+ public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
+ return account.getPrivateKeyAlias();
+ }
+
+ @Override
+ public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ try {
+ return KeyChain.getCertificateChain(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return new X509Certificate[0];
+ }
+ }
+
+ @Override
+ public String[] getClientAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getServerAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ try {
+ return KeyChain.getPrivateKey(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ };
+
+ private OnIqPacketReceived createPacketReceiveHandler() {
+ return new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.setOption(Account.OPTION_REGISTER,
+ false);
+ changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
+ } else if (packet.hasChild("error")
+ && (packet.findChild("error")
+ .hasChild("conflict"))) {
+ changeStatus(Account.State.REGISTRATION_CONFLICT);
+ } else {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ Log.d(Config.LOGTAG, packet.toString());
+ }
+ disconnect(true);
+ }
+ };
+ }
+
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
this.wakeLock = service.getPowerManager().newWakeLock(
@@ -210,7 +286,7 @@ public class XmppConnection implements Runnable {
Tag nextTag;
while ((nextTag = tagReader.readTag()) != null) {
if (nextTag.isStart("stream")) {
- processStream(nextTag);
+ processStream();
break;
} else {
throw new IOException("unknown tag on connect");
@@ -262,11 +338,9 @@ public class XmppConnection implements Runnable {
connect();
}
- private void processStream(final Tag currentTag) throws XmlPullParserException,
- IOException, NoSuchAlgorithmException {
+ private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
Tag nextTag = tagReader.readTag();
-
- while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
+ while (nextTag != null && !nextTag.isEnd("stream")) {
if (nextTag.isStart("error")) {
processStreamError(nextTag);
} else if (nextTag.isStart("features")) {
@@ -286,7 +360,11 @@ public class XmppConnection implements Runnable {
String.valueOf(saslMechanism.getPriority()));
tagReader.reset();
sendStartStream();
- processStream(tagReader.readTag());
+ if (tagReader.readTag().isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after successful auth");
+ }
break;
} else if (nextTag.isStart("failure")) {
throw new UnauthorizedException();
@@ -382,6 +460,7 @@ public class XmppConnection implements Runnable {
}
nextTag = tagReader.readTag();
}
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": last tag was "+nextTag);
if (account.getStatus() == Account.State.ONLINE) {
account. setStatus(Account.State.OFFLINE);
if (statusListener != null) {
@@ -518,13 +597,19 @@ public class XmppConnection implements Runnable {
try {
final SSLContext sc = SSLContext.getInstance("TLS");
MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
- sc.init(null,new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()},mXmppConnectionService.getRNG());
+ KeyManager[] keyManager;
+ if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
+ keyManager = new KeyManager[]{ mKeyManager };
+ } else {
+ keyManager = null;
+ }
+ sc.init(keyManager,new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()},mXmppConnectionService.getRNG());
final SSLSocketFactory factory = sc.getSocketFactory();
final HostnameVerifier verifier;
if (mInteractive) {
- verifier = trustManager.wrapHostnameVerifier(new StrictHostnameVerifier());
+ verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier());
} else {
- verifier = trustManager.wrapHostnameVerifierNonInteractive(new StrictHostnameVerifier());
+ verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier());
}
final InetAddress address = socket == null ? null : socket.getInetAddress();
@@ -562,10 +647,14 @@ public class XmppConnection implements Runnable {
sendStartStream();
Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
features.encryptionEnabled = true;
- processStream(tagReader.readTag());
+ if (tagReader.readTag().isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after STARTTLS");
+ }
sslSocket.close();
} catch (final NoSuchAlgorithmException | KeyManagementException e1) {
- Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException();
}
}
@@ -589,7 +678,9 @@ public class XmppConnection implements Runnable {
.findChild("mechanisms"));
final Element auth = new Element("auth");
auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
- if (mechanisms.contains("SCRAM-SHA-1")) {
+ if (mechanisms.contains("EXTERNAL")) {
+ saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
+ } else if (mechanisms.contains("SCRAM-SHA-1")) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("PLAIN")) {
saslMechanism = new Plain(tagWriter, account);
@@ -643,6 +734,15 @@ public class XmppConnection implements Runnable {
return mechanisms;
}
+ public void sendCaptchaRegistryRequest(String id, Data data) {
+ if (data == null) {
+ setAccountCreationFailed("");
+ } else {
+ IqPacket request = getIqGenerator().generateCreateAccountWithCaptcha(account, id, data);
+ sendIqPacket(request, createPacketReceiveHandler());
+ }
+ }
+
private void sendRegistryRequest() {
final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
register.query("jabber:iq:register");
@@ -651,6 +751,7 @@ public class XmppConnection implements Runnable {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ boolean failed = false;
if (packet.getType() == IqPacket.TYPE.RESULT
&& packet.query().hasChild("username")
&& (packet.query().hasChild("password"))) {
@@ -659,37 +760,57 @@ public class XmppConnection implements Runnable {
final Element password = new Element("password").setContent(account.getPassword());
register.query("jabber:iq:register").addChild(username);
register.query().addChild(password);
- sendIqPacket(register, new OnIqPacketReceived() {
-
- @Override
- public void onIqPacketReceived(final Account account, final IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.RESULT) {
- account.setOption(Account.OPTION_REGISTER,
- false);
- changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
- } else if (packet.hasChild("error")
- && (packet.findChild("error")
- .hasChild("conflict"))) {
- changeStatus(Account.State.REGISTRATION_CONFLICT);
- } else {
- changeStatus(Account.State.REGISTRATION_FAILED);
- Log.d(Config.LOGTAG, packet.toString());
- }
- disconnect(true);
+ sendIqPacket(register, createPacketReceiveHandler());
+ } else if (packet.getType() == IqPacket.TYPE.RESULT
+ && (packet.query().hasChild("x", "jabber:x:data"))) {
+ final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+ final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
+ final String id = packet.getId();
+
+ Bitmap captcha = null;
+ if (blob != null) {
+ try {
+ final String base64Blob = blob.getContent();
+ final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
+ InputStream stream = new ByteArrayInputStream(strBlob);
+ captcha = BitmapFactory.decodeStream(stream);
+ } catch (Exception e) {
+ //ignored
+ }
+ } else {
+ try {
+ Field url = data.getFieldByName("url");
+ String urlString = url.findChildContent("value");
+ URL uri = new URL(urlString);
+ captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
+ } catch(IOException e) {
+ Log.e(Config.LOGTAG, e.toString());
}
- });
+ }
+
+ if (captcha != null) {
+ failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
+ }
} else {
+ failed = true;
+ }
+
+ if (failed) {
final Element instructions = packet.query().findChild("instructions");
- changeStatus(Account.State.REGISTRATION_FAILED);
- disconnect(true);
- Log.d(Config.LOGTAG, account.getJid().toBareJid()
- + ": could not register. instructions are"
- + (instructions != null ? instructions.getContent() : ""));
+ setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
}
}
});
}
+ private void setAccountCreationFailed(String instructions) {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ disconnect(true);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": could not register. instructions are"
+ + instructions);
+ }
+
private void sendBindRequest() {
while(!mXmppConnectionService.areMessagesInitialized()) {
try {
@@ -761,11 +882,9 @@ public class XmppConnection implements Runnable {
this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
- return;
- } else if (packet.getType() == IqPacket.TYPE.RESULT) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
sendPostBindInitialization();
- } else {
+ } else if (packet.getType() != IqPacket.TYPE.TIMEOUT){
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
disconnect(true);
}
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 f989c0c25..a15abe14a 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
@@ -176,7 +176,7 @@ public final class Jid {
return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
} catch (final InvalidJidException e) {
// This should never happen.
- return null;
+ throw new AssertionError("Jid " + this.toString() + " invalid");
}
}
@@ -185,7 +185,7 @@ public final class Jid {
return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
} catch (final InvalidJidException e) {
// This should never happen.
- return null;
+ throw new AssertionError("Jid " + this.toString() + " invalid");
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
index 4f733b10e..388c5dec2 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -737,8 +737,7 @@ public class JingleConnection implements Transferable {
JinglePacket answer = bootstrapPacket("transport-accept");
Content content = new Content("initiator", "a-file-offer");
content.setTransportId(this.transportId);
- content.ibbTransport().setAttribute("block-size",
- Integer.toString(this.ibbBlockSize));
+ content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
answer.setContent(content);
this.sendJinglePacket(answer);
return true;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
index e45e7441d..91cba39f5 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
@@ -3,7 +3,7 @@ package eu.siacs.conversations.xmpp.jingle;
import eu.siacs.conversations.entities.DownloadableFile;
public interface OnFileTransmissionStatusChanged {
- public void onFileTransmitted(DownloadableFile file);
+ void onFileTransmitted(DownloadableFile file);
- public void onFileTransferAborted();
+ void onFileTransferAborted();
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
index 2aaf62a1b..9a60b3924 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
@@ -5,5 +5,5 @@ import eu.siacs.conversations.xmpp.PacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
public interface OnJinglePacketReceived extends PacketReceived {
- public void onJinglePacketReceived(Account account, JinglePacket packet);
+ void onJinglePacketReceived(Account account, JinglePacket packet);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
index 03a437b2b..76e337177 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -1,6 +1,5 @@
package eu.siacs.conversations.xmpp.jingle;
public interface OnPrimaryCandidateFound {
- public void onPrimaryCandidateFound(boolean success,
- JingleCandidate canditate);
+ void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
}