diff options
25 files changed, 586 insertions, 148 deletions
@@ -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 b4dcf209d..bd9ad1a82 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 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/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/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 4d0228e94..7a39bd062 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 dbd5f117b..d962c57ea 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 80e775069..6024177ae 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 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 e3848fe28..0cdae2cdc 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 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..fc9c98122 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 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); } diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml index 2076805ef..62981a454 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 6dd57ae54..ffa692a0b 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 5be800883..162c277bd 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> |