aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java2
-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/generator/IqGenerator.java31
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java124
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java157
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java11
-rw-r--r--src/main/java/eu/siacs/conversations/ui/UiCallback.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java9
-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.java172
-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
-rw-r--r--src/main/res/menu/editaccount.xml7
-rw-r--r--src/main/res/menu/manageaccounts.xml6
-rw-r--r--src/main/res/values/strings.xml8
25 files changed, 586 insertions, 148 deletions
diff --git a/README.md b/README.md
index 90da9f48..12129a20 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Conversations: the very last word in instant messaging
## Features
-* End-to-end encryption with either [OTR](https://otr.cypherpunks.ca/) or [OpenPGP](http://www.openpgp.org/about_openpgp/)
+* End-to-end encryption with [OMEMO](http://conversations.im/omemo/), [OTR](https://otr.cypherpunks.ca/), or [OpenPGP](http://www.openpgp.org/about_openpgp/)
* Send and receive images as well as other kind of files
* Share your location via an external [plug-in](https://play.google.com/store/apps/details?id=eu.siacs.conversations.sharelocation&referrer=utm_source%3Dgithub)
* Indication when your contact has read your message
@@ -236,11 +236,9 @@ I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
### Security
-#### Why are there two end-to-end encryption methods and which one should I choose?
+#### Why are there three end-to-end encryption methods and which one should I choose?
-In most cases OTR should be the encryption method of choice. It works out of the
-box with most contacts as long as they are online. However PGP can, in some
-cases, (message carbons to multiple clients) be more flexible.
+In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. However, openPGP can, in some cases, (message carbons to multiple clients) be more flexible. Unlike OTR, OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations. OMEMO should be preffered over OTR for contacts who use Conversations.
#### How do I use OpenPGP
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index b4dcf209..bd9ad1a8 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -48,6 +48,8 @@ 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 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/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
index 70de2777..58e5a095 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 4d779302..a7831718 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 00000000..df92898c
--- /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 14d8b944..5b4b99ef 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 f47677f6..3a05446c 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/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index 42c57b24..fb69860d 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/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 4d0228e9..7a39bd06 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -29,6 +29,8 @@ 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;
@@ -36,21 +38,17 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
-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 org.bouncycastle.jce.PrincipalUtil;
-import org.bouncycastle.jce.X509Principal;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.math.BigInteger;
-import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateParsingException;
+import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -174,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() {
@@ -257,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
@@ -315,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;
@@ -1304,37 +1304,61 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void run() {
try {
X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
- PrivateKey key = KeyChain.getPrivateKey(XmppConnectionService.this, alias);
- X500Name x500name = new JcaX509CertificateHolder(chain[0]).getSubject();
- String email = IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue());
- String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
- Jid jid = Jid.fromString(email);
- if (findAccountByJid(jid) == null) {
- Account account = new Account(jid, "");
+ 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 (KeyChainException e) {
- callback.informUser(R.string.unable_to_parse_certificate);
- } catch (InterruptedException e) {
- callback.informUser(R.string.unable_to_parse_certificate);
- e.printStackTrace();
- } catch (CertificateEncodingException e) {
- callback.informUser(R.string.unable_to_parse_certificate);
- e.printStackTrace();
- } catch (InvalidJidException e) {
+ } catch (Exception e) {
callback.informUser(R.string.unable_to_parse_certificate);
- e.printStackTrace();
}
}
}).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);
@@ -1459,6 +1483,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()) {
@@ -1563,6 +1612,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);
@@ -2464,6 +2514,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);
@@ -2620,6 +2684,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) {
@@ -2786,6 +2857,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
void onAccountUpdate();
}
+ public interface OnCaptchaRequested {
+ void onCaptchaRequested(Account account,
+ String id,
+ Data data,
+ Bitmap captcha);
+ }
+
public interface OnRosterUpdate {
void onRosterUpdate();
}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index dbd5f117..d962c57e 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,6 +80,7 @@ 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;
@@ -101,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());
}
@@ -176,7 +185,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
&& mAccount.getStatus() != Account.State.ONLINE
&& mFetchingAvatar) {
startActivity(new Intent(getApplicationContext(),
- ManageAccountActivity.class));
+ ManageAccountActivity.class));
finish();
} else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
if (!mFetchingAvatar) {
@@ -195,6 +204,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
public void onAccountUpdate() {
refreshUi();
}
+
private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
@Override
@@ -312,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 "";
@@ -363,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 {
@@ -387,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);
@@ -439,11 +453,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
if (this.mAccount != null) {
if (this.mAccount.getPrivateKeyAlias() != null) {
- this.mPassword.setHint(R.string.authenticate_with_certificate);
- if (this.mInitMode) {
- this.mPassword.requestFocus();
+ this.mPassword.setHint(R.string.authenticate_with_certificate);
+ if (this.mInitMode) {
+ this.mPassword.requestFocus();
+ }
}
- } updateAccountInformation(true);
+ updateAccountInformation(true);
}
} else if (this.xmppConnectionService.getAccounts().size() == 0) {
if (getActionBar() != null) {
@@ -483,16 +498,31 @@ 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());
}
@@ -511,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);
@@ -522,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);
@@ -564,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);
}
@@ -621,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);
@@ -655,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();
@@ -681,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 80e77506..6024177a 100644
--- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -7,7 +7,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
-import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
@@ -103,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);
}
@@ -149,7 +156,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
case R.id.action_enable_all:
enableAllAccounts();
break;
- case R.id.action_add_account_from_key:
+ case R.id.action_add_account_with_cert:
addAccountFromKey();
break;
default:
diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
index c80199e1..d056d628 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 e3848fe2..0cdae2cd 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();
}
@@ -619,6 +625,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 471526af..39bfc082 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 ad7d7622..4be4931f 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 c7c9ac42..e9ad7197 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 c41b6974..fc9c9812 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,13 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
+import java.net.MalformedURLException;
+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 +47,17 @@ 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.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(
@@ -518,7 +594,13 @@ 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) {
@@ -565,7 +647,7 @@ public class XmppConnection implements Runnable {
processStream(tagReader.readTag());
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 +671,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 +727,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 +744,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 +753,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 +875,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 f989c0c2..a15abe14 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 4f733b10..388c5dec 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 e45e7441..91cba39f 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 2aaf62a1..9a60b392 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 03a437b2..76e33717 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);
}
diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml
index 2076805e..62981a45 100644
--- a/src/main/res/menu/editaccount.xml
+++ b/src/main/res/menu/editaccount.xml
@@ -11,6 +11,13 @@
android:showAsAction="never" />
<item
+ android:id="@+id/action_renew_certificate"
+ android:title="@string/action_renew_certificate"
+ android:visible="false"
+ android:showAsAction="never" />
+ />
+
+ <item
android:id="@+id/action_server_info_show_more"
android:title="@string/server_info_show_more"
android:checkable="true"
diff --git a/src/main/res/menu/manageaccounts.xml b/src/main/res/menu/manageaccounts.xml
index 6dd57ae5..ffa692a0 100644
--- a/src/main/res/menu/manageaccounts.xml
+++ b/src/main/res/menu/manageaccounts.xml
@@ -7,11 +7,11 @@
android:showAsAction="always"
android:title="@string/action_add_account"/>
<item
- android:id="@+id/action_add_account_from_key"
+ android:id="@+id/action_add_account_with_cert"
android:showAsAction="never"
android:icon="?attr/icon_add_person"
- android:title="@string/action_add_account_from_key"
- android:visible="false"/>
+ android:title="@string/action_add_account_with_certificate"
+ android:visible="true"/>
<item
android:id="@+id/action_enable_all"
android:title="@string/enable_all_accounts"/>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 5be80088..162c277b 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -524,7 +524,13 @@
<string name="pref_away_when_screen_off_summary">Marks your resource as away when the screen is turned off</string>
<string name="pref_xa_on_silent_mode">Not available in silent mode</string>
<string name="pref_xa_on_silent_mode_summary">Marks your resource as not available when phone is in silent mode</string>
- <string name="action_add_account_from_key">Add account from key</string>
+ <string name="action_add_account_with_certificate">Add account with certificate</string>
<string name="unable_to_parse_certificate">Unable to parse certificate</string>
<string name="authenticate_with_certificate">Leave empty to authenticate w/ certificate</string>
+ <string name="captcha_ocr">Captcha text</string>
+ <string name="captcha_required">Captcha required</string>
+ <string name="captcha_hint">enter the text from the image</string>
+ <string name="certificate_chain_is_not_trusted">Certificate chain is not trusted</string>
+ <string name="jid_does_not_match_certificate">Jabber ID does not match certificate</string>
+ <string name="action_renew_certificate">Renew certificate</string>
</resources>