diff options
Diffstat (limited to 'src/main/java/eu/siacs/conversations')
14 files changed, 258 insertions, 69 deletions
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 e22b05c00..31b23e3cb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -146,8 +146,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { for (Integer deviceId : deviceIds) { AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); - String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", ""); - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint)); + IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } @@ -575,12 +575,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return axolotlStore.getFingerprintTrust(fingerprint); } + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { axolotlStore.setFingerprintTrust(fingerprint, trust); } - private void verifySessionWithPEP(final XmppAxolotlSession session, final IdentityKey identityKey) { + private void verifySessionWithPEP(final XmppAxolotlSession session) { + Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); final AxolotlAddress address = session.getRemoteAddress(); + final IdentityKey identityKey = session.getIdentityKey(); try { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -595,8 +601,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (verifier.verify(verification.second)) { try { mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+session.getFingerprint()); - setFingerprintTrust(session.getFingerprint(), XmppAxolotlSession.Trust.TRUSTED_X509); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); + setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); finishBuildingSessionsFromPEP(address); return; @@ -607,6 +615,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } catch (Exception e) { Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); } + } else { + Log.d(Config.LOGTAG,"no verification found"); } fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); @@ -678,10 +688,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); sessions.put(address, session); if (Config.X509_VERIFICATION) { - verifySessionWithPEP(session, bundle.getIdentityKey()); + verifySessionWithPEP(session); } else { fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); @@ -718,7 +728,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId); @@ -740,7 +750,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); @@ -889,8 +899,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); return (identityKey != null) - ? new XmppAxolotlSession(account, axolotlStore, address, - identityKey.getFingerprint().replaceAll("\\s", "")) + ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) : null; } @@ -944,11 +953,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void putFreshSession(XmppAxolotlSession session) { + Log.d(Config.LOGTAG,"put fresh session"); sessions.put(session); if (Config.X509_VERIFICATION) { - IdentityKey identityKey = axolotlStore.loadSession(session.getRemoteAddress()).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - verifySessionWithPEP(session, identityKey); + if (session.getIdentityKey() != null) { + verifySessionWithPEP(session); } else { Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); } 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 a78317183..3c8cb3c10 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -15,6 +15,7 @@ import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.KeyHelper; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Set; @@ -36,6 +37,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { public static final String NAME = "name"; public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; + public static final String CERTIFICATE = "certificate"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; @@ -213,6 +215,14 @@ public class SQLiteAxolotlStore implements AxolotlStore { trustCache.remove(fingerprint); } + public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { + mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); + } + public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) { return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust); } 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 c452acfd4..b713eb5fe 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -6,6 +6,7 @@ import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.DuplicateMessageException; +import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.InvalidMessageException; @@ -29,7 +30,7 @@ public class XmppAxolotlSession { private final SQLiteAxolotlStore sqLiteAxolotlStore; private final AxolotlAddress remoteAddress; private final Account account; - private String fingerprint = null; + private IdentityKey identityKey; private Integer preKeyId = null; private boolean fresh = true; @@ -103,9 +104,9 @@ public class XmppAxolotlSession { } } - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) { this(account, store, remoteAddress); - this.fingerprint = fingerprint.replaceAll("\\s",""); + this.identityKey = identityKey; } public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { @@ -125,7 +126,11 @@ public class XmppAxolotlSession { } public String getFingerprint() { - return fingerprint; + return identityKey == null ? null : identityKey.getFingerprint().replaceAll("\\s", ""); + } + + public IdentityKey getIdentityKey() { + return identityKey; } public AxolotlAddress getRemoteAddress() { @@ -141,11 +146,11 @@ public class XmppAxolotlSession { } protected void setTrust(Trust trust) { - sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust); + sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust); } protected Trust getTrust() { - Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint); + Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint()); return (trust == null) ? Trust.UNDECIDED : trust; } @@ -164,11 +169,11 @@ public class XmppAxolotlSession { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); - if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint); + IdentityKey msgIdentityKey = message.getIdentityKey(); + if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); } else { - this.fingerprint = fingerprint; + this.identityKey = msgIdentityKey; plaintext = cipher.decrypt(message); if (message.getPreKeyId().isPresent()) { preKeyId = message.getPreKeyId().get(); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index f5fa88d7a..9c8beb1ec 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -104,7 +105,9 @@ public class Contact implements ListItem, Blockable { } public String getDisplayName() { - if (this.systemName != null) { + if (this.presenceName != null && Config.X509_VERIFICATION) { + return this.presenceName; + } else if (this.systemName != null) { return this.systemName; } else if (this.serverName != null) { return this.serverName; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 81f789f8d..bc7f302ae 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -21,6 +21,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -621,8 +622,16 @@ public class Conversation extends AbstractEntity implements Blockable { } public int getNextEncryption() { + final AxolotlService axolotlService = getAccount().getAxolotlService(); int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); if (next == -1) { + if (Config.X509_VERIFICATION && mode == MODE_SINGLE) { + if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { + return Message.ENCRYPTION_AXOLOTL; + } else { + return Message.ENCRYPTION_NONE; + } + } int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); if (outgoing == Message.ENCRYPTION_NONE) { next = this.getMostRecentlyUsedIncomingEncryption(); @@ -631,7 +640,7 @@ public class Conversation extends AbstractEntity implements Blockable { } } if (Config.FORCE_ENCRYPTION && mode == MODE_SINGLE && next <= 0) { - if (getAccount().getAxolotlService().isContactAxolotlCapable(getContact())) { + if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { return Message.ENCRYPTION_AXOLOTL; } else { return Message.ENCRYPTION_OTR; diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e482f0f85..3077c4889 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -19,7 +19,12 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -44,7 +49,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 21; + private static final int DATABASE_VERSION = 22; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -102,6 +107,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + SQLiteAxolotlStore.NAME + " TEXT, " + SQLiteAxolotlStore.OWN + " INTEGER, " + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " + SQLiteAxolotlStore.TRUSTED + " INTEGER, " + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.ACCOUNT @@ -345,6 +351,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { + "=?", new String[]{account.getUuid()}); } } + + if (oldVersion < 22 && newVersion >= 22) { + db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -592,16 +602,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); } - public Pair<Long,String> getLastMessageReceived(Account account) { + public Pair<Long, String> getLastMessageReceived(Account account) { SQLiteDatabase db = this.getReadableDatabase(); String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; String[] args = {account.getUuid()}; - Cursor cursor = db.rawQuery(sql,args); - if (cursor.getCount() ==0) { + Cursor cursor = db.rawQuery(sql, args); + if (cursor.getCount() == 0) { return null; } else { cursor.moveToFirst(); - return new Pair<>(cursor.getLong(0),cursor.getString(1)); + return new Pair<>(cursor.getLong(0), cursor.getString(1)); } } @@ -1050,6 +1060,52 @@ public class DatabaseBackend extends SQLiteOpenHelper { return rows == 1; } + public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + try { + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); + return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs) == 1; + } catch (CertificateEncodingException e) { + Log.d(Config.LOGTAG, "could not encode certificate"); + return false; + } + } + + public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; + String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); + if (cursor.getCount() < 1) { + return null; + } else { + cursor.moveToFirst(); + byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); + if (certificate == null || certificate.length == 0) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); + } catch (CertificateException e) { + Log.d(Config.LOGTAG,"certificate exception "+e.getMessage()); + return null; + } + } + } + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 649ddacd8..28d6e3550 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -50,6 +50,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -992,13 +993,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void onIqPacketReceived(final Account account, final IqPacket packet) { if (packet.getType() == IqPacket.TYPE.RESULT) { final Element query = packet.query(); - final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); + final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); final Element storage = query.findChild("storage", "storage:bookmarks"); if (storage != null) { for (final Element item : storage.getChildren()) { if (item.getName().equals("conference")) { final Bookmark bookmark = Bookmark.parse(item, account); - bookmarks.add(bookmark); + Bookmark old = bookmarks.put(bookmark.getJid(), bookmark); + if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { + bookmark.setBookmarkName(old.getBookmarkName()); + } Conversation conversation = find(bookmark); if (conversation != null) { conversation.setBookmark(bookmark); @@ -1011,7 +1015,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } - account.setBookmarks(bookmarks); + account.setBookmarks(new ArrayList<>(bookmarks.values())); } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks"); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index aff50502c..70cd982b1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -14,6 +14,7 @@ import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -27,15 +28,18 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.QuickContactBadge; import android.widget.TextView; +import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpUtils; +import java.security.cert.X509Certificate; import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +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; @@ -394,7 +398,12 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) { boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight); + hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() { + @Override + public void onClick(View v) { + onOmemoKeyClicked(contact.getAccount(), fingerprint); + } + }); } if (contact.getPgpKeyId() != 0) { hasKeys = true; @@ -446,6 +455,40 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } } + private void onOmemoKeyClicked(Account account, String fingerprint) { + final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint); + if (trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) { + X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint); + if (x509Certificate != null) { + showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate)); + } else { + Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show(); + } + } + } + + private void showCertificateInformationDialog(Bundle bundle) { + View view = getLayoutInflater().inflate(R.layout.certificate_information, null); + final String not_available = getString(R.string.certicate_info_not_available); + TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn); + TextView subject_o = (TextView) view.findViewById(R.id.subject_o); + TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn); + TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o); + TextView sha1 = (TextView) view.findViewById(R.id.sha1); + + subject_cn.setText(bundle.getString("subject_cn", not_available)); + subject_o.setText(bundle.getString("subject_o", not_available)); + issuer_cn.setText(bundle.getString("issuer_cn", not_available)); + issuer_o.setText(bundle.getString("issuer_o", not_available)); + sha1.setText(bundle.getString("sha1", not_available)); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.certificate_information); + builder.setView(view); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } + protected void confirmToDeleteFingerprint(final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.delete_fingerprint); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 47edd1a42..706a2cbd3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -468,7 +468,7 @@ public class ConversationActivity extends XmppActivity menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating()); menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); menuSecure.setVisible(!Config.HIDE_ENCRYPTION_IN_UI); - menuSecure.setVisible(!Config.HIDE_PGP_IN_UI); //if pgp is hidden conferences have no choice of encryption + menuSecure.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); //if pgp is hidden conferences have no choice of encryption } else { menuMucDetails.setVisible(false); } @@ -922,8 +922,9 @@ public class ConversationActivity extends XmppActivity MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none); MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); - pgp.setVisible(!Config.HIDE_PGP_IN_UI); + pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); none.setVisible(!Config.FORCE_ENCRYPTION); + otr.setVisible(!Config.X509_VERIFICATION); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setVisible(false); axolotl.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 439ada3a6..f5356d24f 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -710,7 +710,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate continue; } boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight); + hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null); } if (hasKeys) { keysCard.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 4bd0d42da..2e6d32466 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -126,6 +126,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate // own fingerprints have no impact on locked status. } }, + null, null ); } @@ -140,6 +141,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate lockOrUnlockAsNeeded(); } }, + null, null ); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4ca18cf8e..d05f50817 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -651,7 +651,7 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight) { + protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) { final XmppAxolotlSession.Trust trust = account.getAxolotlService() .getFingerprintTrust(fingerprint); if (trust == null) { @@ -673,7 +673,8 @@ public abstract class XmppActivity extends Activity { XmppAxolotlSession.Trust.UNTRUSTED); v.setEnabled(true); } - } + }, + onKeyClickedListener ); } @@ -685,24 +686,30 @@ public abstract class XmppActivity extends Activity { boolean showTag, CompoundButton.OnCheckedChangeListener onCheckedChangeListener, - View.OnClickListener onClickListener) { + View.OnClickListener onClickListener, + View.OnClickListener onKeyClickedListener) { if (trust == XmppAxolotlSession.Trust.COMPROMISED) { return false; } View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); + key.setOnClickListener(onKeyClickedListener); TextView keyType = (TextView) view.findViewById(R.id.key_type); + keyType.setOnClickListener(onKeyClickedListener); Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust); trustToggle.setVisibility(View.VISIBLE); trustToggle.setOnCheckedChangeListener(onCheckedChangeListener); trustToggle.setOnClickListener(onClickListener); - view.setOnLongClickListener(new View.OnLongClickListener() { + final View.OnLongClickListener purge = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { showPurgeKeyDialog(account, fingerprint); return true; } - }); + }; + view.setOnLongClickListener(purge); + key.setOnLongClickListener(purge); + keyType.setOnLongClickListener(purge); boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509; switch (trust) { case UNTRUSTED: @@ -752,7 +759,7 @@ public abstract class XmppActivity extends Activity { keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); } - key.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); keys.addView(view); return true; } @@ -762,7 +769,7 @@ public abstract class XmppActivity extends Activity { builder.setTitle(getString(R.string.purge_key)); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setMessage(getString(R.string.purge_key_desc_part1) - + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint) + + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2)) + "\n\n" + getString(R.string.purge_key_desc_part2)); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.accept), diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index a3cea867f..a54407dec 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -18,6 +18,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; +import android.util.Patterns; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -32,11 +33,11 @@ import android.widget.Toast; import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import java.util.regex.Matcher; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -246,6 +247,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setText(text); viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayDecryptionFailed(ViewHolder viewHolder, boolean darkBackground) { @@ -258,6 +260,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { R.string.decryption_failed)); viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayHeartMessage(final ViewHolder viewHolder, final String body) { @@ -332,13 +335,21 @@ public class MessageAdapter extends ArrayAdapter<Message> { } viewHolder.messageBody.setText(span); } + int urlCount = 0; + Matcher matcher = Patterns.WEB_URL.matcher(body); + while (matcher.find()) { + urlCount++; + } + viewHolder.messageBody.setTextIsSelectable(urlCount <= 1); } else { viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); } viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? R.color.grey800 : R.color.grey500)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setOnLongClickListener(openContextMenu); } private void displayDownloadableMessage(ViewHolder viewHolder, diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index ab407249c..1ef5fb3f6 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -1,19 +1,17 @@ package eu.siacs.conversations.utils; -import android.util.Log; +import android.os.Bundle; 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 org.bouncycastle.jce.PrincipalUtil; -import java.security.SecureRandom; +import java.security.MessageDigest; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; -import java.security.cert.X509Extension; import java.text.Normalizer; import java.util.ArrayList; import java.util.Arrays; @@ -21,6 +19,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -31,8 +30,6 @@ import eu.siacs.conversations.xmpp.jid.Jid; public final class CryptoHelper { public static final String FILETRANSFER = "?FILETRANSFERv1:"; private final static char[] hexArray = "0123456789abcdef".toCharArray(); - private final static char[] vowels = "aeiou".toCharArray(); - private final static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray(); final public static byte[] ONE = new byte[] { 0, 0, 0, 1 }; public static String bytesToHex(byte[] bytes) { @@ -66,22 +63,6 @@ public final class CryptoHelper { return result; } - public static String randomMucName(SecureRandom random) { - return randomWord(3, random) + "." + randomWord(7, random); - } - - private static String randomWord(int lenght, SecureRandom random) { - StringBuilder builder = new StringBuilder(lenght); - for (int i = 0; i < lenght; ++i) { - if (i % 2 == 0) { - builder.append(consonants[random.nextInt(consonants.length)]); - } else { - builder.append(vowels[random.nextInt(vowels.length)]); - } - } - return builder.toString(); - } - /** * Escapes usernames or passwords for SASL. */ @@ -114,13 +95,21 @@ public final class CryptoHelper { } else if (fingerprint.length() < 40) { return fingerprint; } - StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s","")); + StringBuilder builder = new StringBuilder(fingerprint.toLowerCase(Locale.US).replaceAll("\\s", "")); for(int i=8;i<builder.length();i+=9) { builder.insert(i, ' '); } return builder.toString(); } + public static String prettifyFingerprintCert(String fingerprint) { + StringBuilder builder = new StringBuilder(fingerprint); + for(int i=2;i < builder.length(); i+=3) { + builder.insert(i,':'); + } + return builder.toString(); + } + public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites); @@ -167,6 +156,46 @@ public final class CryptoHelper { } } + public static Bundle extractCertificateInformation(X509Certificate certificate) { + Bundle information = new Bundle(); + try { + JcaX509CertificateHolder holder = new JcaX509CertificateHolder(certificate); + X500Name subject = holder.getSubject(); + try { + information.putString("subject_cn", subject.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + information.putString("subject_o",subject.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + + X500Name issuer = holder.getIssuer(); + try { + information.putString("issuer_cn", issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + information.putString("issuer_o", issuer.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] fingerprint = md.digest(certificate.getEncoded()); + information.putString("sha1", prettifyFingerprintCert(bytesToHex(fingerprint))); + } catch (Exception e) { + + } + return information; + } catch (CertificateEncodingException e) { + return information; + } + } + public static int encryptionTypeToText(int encryption) { switch (encryption) { case Message.ENCRYPTION_OTR: |