diff options
20 files changed, 462 insertions, 293 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 33069439..a918811a 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -78,6 +78,10 @@ public final class Config { public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096; + public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + + public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY; + public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance @@ -97,7 +101,6 @@ public final class Config { public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesn’t filter - 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/OmemoActivity.java b/src/main/java/eu/siacs/conversations/OmemoActivity.java index c4177cd2..5cda7e5f 100644 --- a/src/main/java/eu/siacs/conversations/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/OmemoActivity.java @@ -15,6 +15,7 @@ import android.widget.Toast; import java.security.cert.X509Certificate; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.widget.Switch; @@ -60,9 +61,17 @@ public abstract class OmemoActivity extends XmppActivity { } } - protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight) { - final FingerprintStatus status = account.getAxolotlService().getFingerprintTrust(fingerprint); - return status != null && addFingerprintRowWithListeners(keys, account, fingerprint, highlight, status, true, true, new CompoundButton.OnCheckedChangeListener() { + protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) { + final Account account = session.getAccount(); + final String fingerprint = session.getFingerprint(); + addFingerprintRowWithListeners(keys, + session.getAccount(), + session.getFingerprint(), + highlight, + session.getTrust(), + true, + true, + new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked)); @@ -70,7 +79,7 @@ public abstract class OmemoActivity extends XmppActivity { }); } - protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account, + protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, FingerprintStatus status, @@ -78,9 +87,6 @@ public abstract class OmemoActivity extends XmppActivity { boolean undecidedNeedEnablement, CompoundButton.OnCheckedChangeListener onCheckedChangeListener) { - if (status.isCompromised()) { - return false; - } View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); @@ -184,7 +190,6 @@ public abstract class OmemoActivity extends XmppActivity { key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); keys.addView(view); - return true; } public void showPurgeKeyDialog(final Account account, final String fingerprint) { 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 702d4ada..377d26b9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -25,13 +25,17 @@ import java.security.PrivateKey; import java.security.Security; import java.security.Signature; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -73,6 +77,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private int numPublishTriesOnEmptyPep = 0; private boolean pepBroken = false; + private AtomicBoolean ownPushPending = new AtomicBoolean(false); + @Override public void onAdvancedStreamFeaturesAvailable(Account account) { if (Config.supportOmemo() @@ -172,7 +178,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { for (Integer deviceId : deviceIds) { AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); if(Config.X509_VERIFICATION) { X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); @@ -297,16 +302,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return new AxolotlAddress(jid.toPreppedString(), 0); } - public Set<XmppAxolotlSession> findOwnSessions() { + public Collection<XmppAxolotlSession> findOwnSessions() { AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); - return new HashSet<>(this.sessions.getAll(ownAddress).values()); + ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress).values()); + Collections.sort(s); + return s; } - private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) { + public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) { AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - return new HashSet<>(this.sessions.getAll(contactAddress).values()); + ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress).values()); + Collections.sort(s); + return s; } private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) { @@ -317,22 +326,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return sessions; } - public Set<String> getFingerprintsForOwnSessions() { - Set<String> fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findOwnSessions()) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } - - public Set<String> getFingerprintsForContact(final Contact contact) { - Set<String> fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findSessionsForContact(contact)) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } - private boolean hasAny(Jid jid) { return sessions.hasAny(getAddressForJid(jid)); } @@ -366,23 +359,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { - if (jid.toBareJid().equals(account.getJid().toBareJid())) { - if (!deviceIds.isEmpty()) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attempts and pepBroken status."); - pepBroken = false; - numPublishTriesOnEmptyPep = 0; - } - if (deviceIds.contains(getOwnDeviceId())) { - deviceIds.remove(getOwnDeviceId()); - } else { - publishOwnDeviceId(deviceIds); - } - for (Integer deviceId : deviceIds) { - AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); - if (sessions.get(ownDeviceAddress) == null) { - buildSessionFromPEP(ownDeviceAddress); - } - } + boolean me = jid.toBareJid().equals(account.getJid().toBareJid()); + if (me && ownPushPending.getAndSet(false)) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring own device update because of pending push"); + return; + } + boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId()); + if (me) { + deviceIds.remove(getOwnDeviceId()); } Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString())); expiredDevices.removeAll(deviceIds); @@ -401,10 +385,25 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { XmppAxolotlSession session = sessions.get(address); if (session != null && session.getFingerprint() != null) { if (!session.getTrust().isActive()) { + Log.d(Config.LOGTAG,"reactivating device with fingprint "+session.getFingerprint()); session.setTrust(session.getTrust().toActive()); } } } + if (me) { + if (Config.OMEMO_AUTO_EXPIRY != 0) { + needsPublishing |= deviceIds.removeAll(getExpiredDevices()); + } + for (Integer deviceId : deviceIds) { + AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + buildSessionFromPEP(ownDeviceAddress); + } + } + if (needsPublishing) { + publishOwnDeviceId(deviceIds); + } + } this.deviceIds.put(jid, deviceIds); mXmppConnectionService.keyStatusUpdated(null); } @@ -418,12 +417,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - } - }); + mXmppConnectionService.sendIqPacket(account, publish, null); } public void purgeKey(final String fingerprint) { @@ -444,14 +438,30 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else { Element item = mXmppConnectionService.getIqParser().getItem(packet); Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if (!deviceIds.contains(getOwnDeviceId())) { - publishOwnDeviceId(deviceIds); - } + registerDevices(account.getJid().toBareJid(),deviceIds); } } }); } + private Set<Integer> getExpiredDevices() { + Set<Integer> devices = new HashSet<>(); + for(XmppAxolotlSession session : findOwnSessions()) { + if (session.getTrust().isActive()) { + long diff = System.currentTimeMillis() - session.getTrust().getLastActivation(); + if (diff > Config.OMEMO_AUTO_EXPIRY) { + long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint()); + if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) { + devices.add(session.getRemoteAddress().getDeviceId()); + session.setTrust(session.getTrust().toInactive()); + Log.d(Config.LOGTAG, "added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+(lastMessageDiff/1000)+"s ago"); + } + } + } + } + return devices; + } + public void publishOwnDeviceId(Set<Integer> deviceIds) { Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds); if (!deviceIdsCopy.contains(getOwnDeviceId())) { @@ -470,9 +480,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } deviceIdsCopy.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy); + ownPushPending.set(true); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { + ownPushPending.set(false); if (packet.getType() == IqPacket.TYPE.ERROR) { pepBroken = true; Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); @@ -954,18 +966,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { account.getJid().toBareJid(), getOwnDeviceId()); Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation); - Set<XmppAxolotlSession> ownSessions = findOwnSessions(); + Collection<XmppAxolotlSession> ownSessions = findOwnSessions(); if (remoteSessions.isEmpty()) { return null; } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); for (XmppAxolotlSession session : remoteSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); axolotlMessage.addDevice(session); } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements..."); for (XmppAxolotlSession session : ownSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); axolotlMessage.addDevice(session); } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java index b594b5de..cfd3b214 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java @@ -3,10 +3,13 @@ package eu.siacs.conversations.crypto.axolotl; import android.content.ContentValues; import android.database.Cursor; -public class FingerprintStatus { +public class FingerprintStatus implements Comparable<FingerprintStatus> { + + private static final long DO_NOT_OVERWRITE = -1; private Trust trust = Trust.UNTRUSTED; private boolean active = false; + private long lastActivation = DO_NOT_OVERWRITE; @Override public boolean equals(Object o) { @@ -34,6 +37,9 @@ public class FingerprintStatus { final ContentValues contentValues = new ContentValues(); contentValues.put(SQLiteAxolotlStore.TRUST,trust.toString()); contentValues.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0); + if (lastActivation != DO_NOT_OVERWRITE) { + contentValues.put(SQLiteAxolotlStore.LAST_ACTIVATION,lastActivation); + } return contentValues; } @@ -45,6 +51,7 @@ public class FingerprintStatus { status.trust = Trust.UNTRUSTED; } status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0; + status.lastActivation = cursor.getLong(cursor.getColumnIndex(SQLiteAxolotlStore.LAST_ACTIVATION)); return status; } @@ -52,6 +59,7 @@ public class FingerprintStatus { final FingerprintStatus status = new FingerprintStatus(); status.trust = Trust.UNDECIDED; status.active = true; + status.lastActivation = System.currentTimeMillis(); return status; } @@ -92,6 +100,9 @@ public class FingerprintStatus { public FingerprintStatus toActive() { FingerprintStatus status = new FingerprintStatus(); status.trust = trust; + if (!status.active) { + status.lastActivation = System.currentTimeMillis(); + } status.active = true; return status; } @@ -128,6 +139,27 @@ public class FingerprintStatus { return status; } + @Override + public int compareTo(FingerprintStatus o) { + if (active == o.active) { + if (lastActivation > o.lastActivation) { + return -1; + } else if (lastActivation < o.lastActivation) { + return 1; + } else { + return 0; + } + } else if (active){ + return -1; + } else { + return 1; + } + } + + public long getLastActivation() { + return lastActivation; + } + public enum Trust { COMPROMISED, UNDECIDED, 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 a3647be7..cd605248 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -38,6 +38,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { public static final String TRUSTED = "trusted"; //no longer used public static final String TRUST = "trust"; public static final String ACTIVE = "active"; + public static final String LAST_ACTIVATION = "last_activation"; public static final String OWN = "ownkey"; public static final String CERTIFICATE = "certificate"; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java index 572d62c8..725757a3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -22,7 +22,7 @@ import org.whispersystems.libaxolotl.protocol.WhisperMessage; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -public class XmppAxolotlSession { +public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> { private final SessionCipher cipher; private final SQLiteAxolotlStore sqLiteAxolotlStore; private final AxolotlAddress remoteAddress; @@ -132,4 +132,13 @@ public class XmppAxolotlSession { return null; } } + + public Account getAccount() { + return account; + } + + @Override + public int compareTo(XmppAxolotlSession o) { + return getTrust().compareTo(o.getTrust()); + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 77828b03..e0bb6d1d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; @@ -489,7 +490,7 @@ public class Account extends AbstractEntity { if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { return null; } - this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey); + this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US); return this.otrFingerprint; } catch (final OtrCryptoException ignored) { return null; diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 55f5443a..72ce1a5a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -301,7 +301,7 @@ public class Contact implements ListItem, Blockable { for (int i = 0; i < prints.length(); ++i) { final String print = prints.isNull(i) ? null : prints.getString(i); if (print != null && !print.isEmpty()) { - fingerprints.add(prints.getString(i)); + fingerprints.add(prints.getString(i).toLowerCase(Locale.US)); } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 245ff288..9605fefc 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; +import java.util.Locale; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpDecryptionService; @@ -627,7 +628,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); - this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey); + this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US); } catch (final OtrCryptoException | UnsupportedOperationException ignored) { return null; } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 1a417f32..18c60bff 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -24,6 +24,7 @@ import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SSLSocketHelper; +import eu.siacs.conversations.utils.TLSSocketFactory; public class HttpConnectionManager extends AbstractConnectionManager { @@ -77,18 +78,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { new StrictHostnameVerifier()); } try { - final SSLContext sc = SSLSocketHelper.getSSLContext(); - sc.init(null, new X509TrustManager[]{trustManager}, - mXmppConnectionService.getRNG()); - - final SSLSocketFactory sf = sc.getSocketFactory(); - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sf.getSupportedCipherSuites()); - if (cipherSuites.length > 0) { - sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); - - } - + final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); connection.setSSLSocketFactory(sf); connection.setHostnameVerifier(hostnameVerifier); } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 1e50ce9c..c111efa4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -55,7 +55,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 31; + private static final int DATABASE_VERSION = 32; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -132,6 +132,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " + SQLiteAxolotlStore.TRUST + " TEXT, " + SQLiteAxolotlStore.ACTIVE + " NUMBER, " + + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER," + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " @@ -377,6 +378,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } + if (oldVersion < 32 && newVersion >= 32) { + db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER"); + ContentValues defaults = new ContentValues(); + defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis()); + db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null); + } } private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) { @@ -780,6 +787,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } + public long getLastTimeFingerprintUsed(Account account, String fingerprint) { + String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1"; + String[] args = {account.getUuid(), fingerprint}; + Cursor cursor = getReadableDatabase().rawQuery(SQL,args); + long time; + if (cursor.moveToFirst()) { + time = cursor.getLong(0); + } else { + time = 0; + } + cursor.close(); + return time; + } + public Pair<Long,String> getLastClearDate(Account account) { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {Conversation.ATTRIBUTES}; @@ -1046,6 +1067,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { String[] columns = {SQLiteAxolotlStore.TRUST, SQLiteAxolotlStore.ACTIVE, + SQLiteAxolotlStore.LAST_ACTIVATION, SQLiteAxolotlStore.KEY}; ArrayList<String> selectionArgs = new ArrayList<>(4); selectionArgs.add(account.getUuid()); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1321485d..8c691102 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -67,7 +67,6 @@ import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; @@ -335,32 +334,34 @@ public class XmppConnectionService extends Service { } account.pendingConferenceJoins.clear(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { - resetSendingToWaiting(account); - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - synchronized (mLowPingTimeoutMode) { - if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now"); - reconnectAccount(account, true, false); - } else { - int timeToReconnect = mRandom.nextInt(20) + 10; - scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); + } else { + if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { + resetSendingToWaiting(account); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + synchronized (mLowPingTimeoutMode) { + if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now"); + reconnectAccount(account, true, false); + } else { + int timeToReconnect = mRandom.nextInt(10) + 2; + scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); + } } } - } - } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { - databaseBackend.updateAccount(account); - reconnectAccount(account, true, false); - } else if ((account.getStatus() != Account.State.CONNECTING) - && (account.getStatus() != Account.State.NO_INTERNET)) { - resetSendingToWaiting(account); - if (connection != null) { - int next = connection.getTimeToNextAttempt(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": error connecting account. try again in " - + next + "s for the " - + (connection.getAttempt() + 1) + " time"); - scheduleWakeUpCall(next, account.getUuid().hashCode()); + } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { + databaseBackend.updateAccount(account); + reconnectAccount(account, true, false); + } else if ((account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.NO_INTERNET)) { + resetSendingToWaiting(account); + if (connection != null) { + int next = connection.getTimeToNextAttempt(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": error connecting account. try again in " + + next + "s for the " + + (connection.getAttempt() + 1) + " time"); + scheduleWakeUpCall(next, account.getUuid().hashCode()); + } } } getNotificationService().updateErrorNotification(); @@ -547,7 +548,7 @@ public class XmppConnectionService extends Service { switch (action) { case ConnectivityManager.CONNECTIVITY_ACTION: if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { - resetAllAttemptCounts(true); + resetAllAttemptCounts(true, false); } break; case ACTION_MERGE_PHONE_CONTACTS: @@ -573,7 +574,7 @@ public class XmppConnectionService extends Service { dismissErrorNotifications(); break; case ACTION_TRY_AGAIN: - resetAllAttemptCounts(false); + resetAllAttemptCounts(false, true); interactive = true; break; case ACTION_REPLY_TO_CONVERSATION: @@ -611,103 +612,108 @@ public class XmppConnectionService extends Service { break; } } - this.wakeLock.acquire(); + synchronized (this) { + this.wakeLock.acquire(); + boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action); + HashSet<Account> pingCandidates = new HashSet<>(); + for (Account account : accounts) { + pingNow |= processAccountState(account, + interactive, + "ui".equals(action), + CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash), + pingCandidates); + } + if (pingNow) { + for (Account account : pingCandidates) { + final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()); + account.getXmppConnection().sendPing(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")"); + scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (final RuntimeException ignored) { + } + } + } + return START_STICKY; + } + private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) { boolean pingNow = false; - HashSet<Account> pingCandidates = new HashSet<>(); - - for (Account account : accounts) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - if (!hasInternetConnection()) { - account.setStatus(Account.State.NO_INTERNET); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!hasInternetConnection()) { + account.setStatus(Account.State.NO_INTERNET); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } else { + if (account.getStatus() == Account.State.NO_INTERNET) { + account.setStatus(Account.State.OFFLINE); if (statusListener != null) { statusListener.onStatusChanged(account); } - } else { - if (account.getStatus() == Account.State.NO_INTERNET) { - account.setStatus(Account.State.OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - long lastReceived = account.getXmppConnection().getLastPacketReceived(); - long lastSent = account.getXmppConnection().getLastPingSent(); - long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; - long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); - int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; - long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); - if (lastSent > lastReceived) { - if (pingTimeoutIn < 0) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); - this.reconnectAccount(account, true, interactive); - } else { - int secs = (int) (pingTimeoutIn / 1000); - this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); + } + if (account.getStatus() == Account.State.ONLINE) { + synchronized (mLowPingTimeoutMode) { + long lastReceived = account.getXmppConnection().getLastPacketReceived(); + long lastSent = account.getXmppConnection().getLastPingSent(); + long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; + long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); + int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; + long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); + if (lastSent > lastReceived) { + if (pingTimeoutIn < 0) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); + this.reconnectAccount(account, true, interactive); + } else { + int secs = (int) (pingTimeoutIn / 1000); + this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); + } + } else { + pingCandidates.add(account); + if (isAccountPushed) { + pingNow = true; + if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode"); } + } else if (msToNextPing <= 0) { + pingNow = true; } else { - pingCandidates.add(account); - if (CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash)) { - pingNow = true; - if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode"); - } - } else if (msToNextPing <= 0) { - pingNow = true; - } else { - this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); - if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode"); - } + this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); + if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode"); } } } - } else if (account.getStatus() == Account.State.OFFLINE) { + } + } else if (account.getStatus() == Account.State.OFFLINE) { + reconnectAccount(account, true, interactive); + } else if (account.getStatus() == Account.State.CONNECTING) { + long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; + long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; + long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; + long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; + if (timeout < 0) { + Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")"); + account.getXmppConnection().resetAttemptCount(false); reconnectAccount(account, true, interactive); - } else if (account.getStatus() == Account.State.CONNECTING) { - long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; - long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; - long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; - long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; - if (timeout < 0) { - Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); - account.getXmppConnection().resetAttemptCount(); - reconnectAccount(account, true, interactive); - } else if (discoTimeout < 0) { - account.getXmppConnection().sendDiscoTimeout(); - scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); - } else { - scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); - } + } else if (discoTimeout < 0) { + account.getXmppConnection().sendDiscoTimeout(); + scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); } else { - if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { - reconnectAccount(account, true, interactive); - } + scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); + } + } else { + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true, interactive); } } - if (mOnAccountUpdate != null) { - mOnAccountUpdate.onAccountUpdate(); - } - } - } - if (pingNow) { - for (Account account : pingCandidates) { - synchronized (mLowPingTimeoutMode) { - final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()); - account.getXmppConnection().sendPing(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")"); - scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); - } - } - } - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (final RuntimeException ignored) { } } - return START_STICKY; + return pingNow; } public boolean isDataSaverDisabled() { @@ -814,13 +820,13 @@ public class XmppConnectionService extends Service { } } - private void resetAllAttemptCounts(boolean reallyAll) { + private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) { Log.d(Config.LOGTAG, "resetting all attempt counts"); for (Account account : accounts) { if (account.hasErrorStatus() || reallyAll) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { - connection.resetAttemptCount(); + connection.resetAttemptCount(retryImmediately); } } if (account.setShowErrorNotification(true)) { @@ -2890,8 +2896,6 @@ public class XmppConnectionService extends Service { if (connection == null) { connection = createConnection(account); account.setXmppConnection(connection); - } else { - connection.interrupt(); } if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!force) { @@ -2900,6 +2904,7 @@ public class XmppConnectionService extends Service { Thread thread = new Thread(connection); connection.setInteractive(interactive); connection.prepareNewConnection(); + connection.interrupt(); thread.start(); scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index a567f151..b8313d9e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -41,6 +41,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -445,9 +446,12 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } } if (Config.supportOmemo()) { - for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) { - boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight); + for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) { + if (!session.getTrust().isCompromised()) { + boolean highlight = session.getFingerprint().equals(messageFingerprint); + hasKeys = true; + addFingerprintRow(keys, session, highlight); + } } } if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 0f38173b..693b7185 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -44,6 +44,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.OmemoActivity; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.services.XmppConnectionService; @@ -251,6 +252,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private TableRow mPushRow; private String mSavedInstanceAccount; private boolean mSavedInstanceInit = false; + private Button mClearDevicesButton; public void refreshUiReal() { invalidateOptionsMenu(); @@ -502,6 +504,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.mNamePort = (LinearLayout) findViewById(R.id.name_port); this.mHostname = (EditText) findViewById(R.id.hostname); this.mHostname.addTextChangedListener(mTextWatcher); + this.mClearDevicesButton = (Button) findViewById(R.id.clear_devices); + this.mClearDevicesButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showWipePepDialog(); + } + }); this.mPort = (EditText) findViewById(R.id.port); this.mPort.setText("5222"); this.mPort.addTextChangedListener(mTextWatcher); @@ -540,7 +549,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat 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 showPassword = menu.findItem(R.id.action_show_password); - final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices); final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate); final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs); final MenuItem changePresence = menu.findItem(R.id.action_change_presence); @@ -554,17 +562,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat changePassword.setVisible(false); } mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam()); - Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); - if (otherDevices == null || otherDevices.isEmpty() || !Config.supportOmemo()) { - clearDevices.setVisible(false); - } changePresence.setVisible(manuallyChangePresence()); } else { showQrCode.setVisible(false); showBlocklist.setVisible(false); showMoreInfo.setVisible(false); changePassword.setVisible(false); - clearDevices.setVisible(false); mamPrefs.setVisible(false); changePresence.setVisible(false); } @@ -706,9 +709,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat case R.id.action_mam_prefs: editMamPrefs(); break; - case R.id.action_clear_devices: - showWipePepDialog(); - break; case R.id.action_renew_certificate: renewCertificate(); break; @@ -904,15 +904,21 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } boolean hasKeys = false; keys.removeAllViews(); - for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) { - if (ownAxolotlFingerprint.equals(fingerprint)) { - continue; + for(XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { + if (!session.getTrust().isCompromised()) { + boolean highlight = session.getFingerprint().equals(messageFingerprint); + addFingerprintRow(keys,session,highlight); + hasKeys = true; } - boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight); } if (hasKeys && Config.supportOmemo()) { keysCard.setVisibility(View.VISIBLE); + Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); + if (otherDevices == null || otherDevices.isEmpty()) { + mClearDevicesButton.setVisibility(View.GONE); + } else { + mClearDevicesButton.setVisibility(View.VISIBLE); + } } else { keysCard.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 2d24fbbf..a55863fd 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -401,7 +401,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU EnterJidDialog dialog = new EnterJidDialog( this, mKnownHosts, mActivatedAccounts, getString(R.string.create_contact), getString(R.string.create), - prefilledJid, null, !invite.hasFingerprints() + prefilledJid, null, invite == null || !invite.hasFingerprints() ); dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() { @@ -420,8 +420,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU if (contact.showInRoster()) { throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists)); } else { - //contact.addOtrFingerprint(fingerprint); xmppConnectionService.createContact(contact); + if (invite != null && invite.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints()); + } switchToConversation(contact); return true; } @@ -843,7 +845,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU private boolean handleJid(Invite invite) { Account account = xmppConnectionService.findAccountByJid(invite.getJid()); - if (account != null && invite.hasFingerprints()) { + if (account != null && !account.isOptionSet(Account.OPTION_DISABLED) && invite.hasFingerprints()) { if (xmppConnectionService.verifyFingerprints(account,invite.getFingerprints())) { switchToAccount(account); finish(); diff --git a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java new file mode 100644 index 00000000..9d8a4111 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java @@ -0,0 +1,69 @@ +package eu.siacs.conversations.utils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +public class TLSSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory internalSSLSocketFactory; + + public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustManager, random); + this.internalSSLSocketFactory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return CryptoHelper.getOrderedCipherSuites(internalSSLSocketFactory.getDefaultCipherSuites()); + } + + @Override + public String[] getSupportedCipherSuites() { + return internalSSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private static Socket enableTLSOnSocket(Socket socket) { + if(socket != null && (socket instanceof SSLSocket)) { + try { + SSLSocketHelper.setSecurity((SSLSocket) socket); + } catch (NoSuchAlgorithmException e) { + //ignoring + } + } + return socket; + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 1fc9812a..8cf5eeac 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -85,7 +85,7 @@ public class XmppUri { String[] parts = pair.split("=",2); if (parts.length == 2) { String key = parts[0].toLowerCase(Locale.US); - String value = parts[1]; + String value = parts[1].toLowerCase(Locale.US); if (OTR_URI_PARAM.equals(key)) { fingerprints.add(new Fingerprint(FingerprintType.OTR,value)); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d6a3b2cb..128e1187 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -218,7 +218,10 @@ public class XmppConnection implements Runnable { mXmppConnectionService = service; } - protected void changeStatus(final Account.State nextStatus) { + protected synchronized void changeStatus(final Account.State nextStatus) { + if (Thread.currentThread().isInterrupted()) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not changing status to "+nextStatus+" because thread was interrupted"); + } if (account.getStatus() != nextStatus) { if ((nextStatus == Account.State.OFFLINE) && (account.getStatus() != Account.State.CONNECTING) @@ -262,6 +265,7 @@ public class XmppConnection implements Runnable { break; } try { + Socket localSocket; shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); @@ -276,8 +280,15 @@ public class XmppConnection implements Runnable { destination = account.getHostname(); } Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor"); - socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); - startXmpp(); + localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); + try { + startXmpp(localSocket); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream"); + return; + } catch (Exception e) { + throw new IOException(e.getMessage()); + } } else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) { InetSocketAddress address = new InetSocketAddress(account.getHostname(), account.getPort()); @@ -288,33 +299,47 @@ public class XmppConnection implements Runnable { if (features.encryptionEnabled) { try { final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - socket = tlsFactoryVerifier.factory.createSocket(); - socket.connect(address, Config.SOCKET_TIMEOUT * 1000); - final SSLSession session = ((SSLSocket) socket).getSession(); + localSocket = tlsFactoryVerifier.factory.createSocket(); + localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000); + final SSLSession session = ((SSLSocket) localSocket).getSession(); if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), session)) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); throw new SecurityException(); } } catch (KeyManagementException e) { features.encryptionEnabled = false; - socket = new Socket(); + localSocket = new Socket(); } } else { - socket = new Socket(); - socket.connect(address, Config.SOCKET_TIMEOUT * 1000); + localSocket = new Socket(); + localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000); } } catch (IOException e) { throw new UnknownHostException(); } - startXmpp(); + try { + startXmpp(localSocket); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream"); + return; + } catch (Exception e) { + throw new IOException(e.getMessage()); + } } else if (DNSHelper.isIp(account.getServer().toString())) { - socket = new Socket(); + localSocket = new Socket(); try { - socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000); + localSocket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000); } catch (IOException e) { throw new UnknownHostException(); } - startXmpp(); + try { + startXmpp(localSocket); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream"); + return; + } catch (Exception e) { + throw new IOException(e.getMessage()); + } } else { final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService); final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); @@ -350,32 +375,37 @@ public class XmppConnection implements Runnable { } if (!features.encryptionEnabled) { - socket = new Socket(); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + localSocket = new Socket(); + localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); } else { final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - socket = tlsFactoryVerifier.factory.createSocket(); + localSocket = tlsFactoryVerifier.factory.createSocket(); - if (socket == null) { + if (localSocket == null) { throw new IOException("could not initialize ssl socket"); } - SSLSocketHelper.setSecurity((SSLSocket) socket); - SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); - SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); + SSLSocketHelper.setSecurity((SSLSocket) localSocket); + SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer().getDomainpart()); + SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client"); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { + if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) localSocket).getSession())) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); throw new SecurityException(); } } - - if (startXmpp()) + if (startXmpp(localSocket)) { break; // successfully connected to server that speaks xmpp + } else { + localSocket.close(); + } } catch (final SecurityException e) { throw e; + } catch (InterruptedException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream"); + return; } catch (final Throwable e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")"); if (!iterator.hasNext()) { @@ -385,8 +415,10 @@ public class XmppConnection implements Runnable { } } processStream(); - } catch (final java.lang.SecurityException e) { + } catch (final java.lang.SecurityException e) { this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); + } catch (final RegistrationNotSupportedException e) { + this.changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); } catch (final IncompatibleServerException e) { this.changeStatus(Account.State.INCOMPATIBLE_SERVER); } catch (final SecurityException e) { @@ -410,12 +442,16 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.OFFLINE); this.attempt = Math.max(0, this.attempt - 1); } finally { - forceCloseSocket(); - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (final RuntimeException ignored) { + if (!Thread.currentThread().isInterrupted()) { + forceCloseSocket(); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (final RuntimeException ignored) { + } } + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not force closing socket and releasing wake lock because thread was interrupted"); } } } @@ -423,27 +459,18 @@ public class XmppConnection implements Runnable { /** * Starts xmpp protocol, call after connecting to socket * @return true if server returns with valid xmpp, false otherwise - * @throws IOException Unknown tag on connect - * @throws XmlPullParserException Bad Xml - * @throws NoSuchAlgorithmException Other error */ - private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException { + private boolean startXmpp(Socket socket) throws Exception { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + this.socket = socket; tagWriter.setOutputStream(socket.getOutputStream()); tagReader.setInputStream(socket.getInputStream()); tagWriter.beginDocument(); sendStartStream(); - Tag nextTag; - while ((nextTag = tagReader.readTag()) != null) { - if (nextTag.isStart("stream")) { - return true; - } else { - throw new IOException("unknown tag on connect"); - } - } - if (socket.isConnected()) { - socket.close(); - } - return false; + final Tag tag = tagReader.readTag(); + return tag != null && tag.isStart("stream"); } private static class TlsFactoryVerifier { @@ -812,10 +839,8 @@ public class XmppConnection implements Runnable { } else { throw new IncompatibleServerException(); } - } else if (!this.streamFeatures.hasChild("register") - && account.isOptionSet(Account.OPTION_REGISTER)) { - forceCloseSocket(); - changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); + } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { + throw new RegistrationNotSupportedException(); } else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) { @@ -950,7 +975,7 @@ public class XmppConnection implements Runnable { } public void resetEverything() { - resetAttemptCount(); + resetAttemptCount(true); resetStreamId(); clearIqCallbacks(); mStanzaQueue.clear(); @@ -1359,30 +1384,6 @@ public class XmppConnection implements Runnable { } } - public void waitForPush() { - if (tagWriter.isActive()) { - tagWriter.finish(); - new Thread(new Runnable() { - @Override - public void run() { - try { - while(!tagWriter.finished()) { - Thread.sleep(10); - } - socket.close(); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream"); - changeStatus(Account.State.OFFLINE); - } catch (IOException | InterruptedException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": error while closing socket for waitForPush()"); - } - } - }).start(); - } else { - forceCloseSocket(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)"); - } - } - private void forceCloseSocket() { if (socket != null) { try { @@ -1526,9 +1527,11 @@ public class XmppConnection implements Runnable { this.sendPacket(new InactivePacket()); } - public void resetAttemptCount() { + public void resetAttemptCount(boolean resetConnectTime) { this.attempt = 0; - this.lastConnect = 0; + if (resetConnectTime) { + this.lastConnect = 0; + } } public void setInteractive(boolean interactive) { @@ -1567,6 +1570,10 @@ public class XmppConnection implements Runnable { } + private class RegistrationNotSupportedException extends IOException { + + } + public enum Identity { FACEBOOK, SLACK, diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index ea4d49c8..6c26a162 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -610,6 +610,14 @@ android:orientation="vertical" android:showDividers="middle"> </LinearLayout> + <Button + android:id="@+id/clear_devices" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/clear_other_devices" + android:layout_gravity="center_horizontal" + android:textColor="@color/accent"/> </LinearLayout> </LinearLayout> </ScrollView> diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml index 9ab1788b..5df7ee5b 100644 --- a/src/main/res/menu/editaccount.xml +++ b/src/main/res/menu/editaccount.xml @@ -44,10 +44,6 @@ android:title="@string/change_password"/> <item - android:id="@+id/action_clear_devices" - android:showAsAction="never" - android:title="@string/clear_other_devices"/> - <item android:id="@+id/action_settings" android:orderInCategory="100" android:showAsAction="never" |