From b8048a5538293b3855c2b2eae55d35645e614f11 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 29 May 2015 11:17:26 +0200 Subject: CryptoNext persistance layer mockup Initial sketch of the peripheral storage infrastructure for the new axolotl-based encryption scheme. --- build.gradle | 1 + .../crypto/axolotl/AxolotlService.java | 440 +++++++++++++++++++++ .../crypto/axolotl/XmppAxolotlMessage.java | 4 + .../eu/siacs/conversations/entities/Account.java | 4 + .../eu/siacs/conversations/entities/Contact.java | 182 ++++++--- .../eu/siacs/conversations/entities/Message.java | 13 + .../conversations/persistance/DatabaseBackend.java | 263 +++++++++++- 7 files changed, 842 insertions(+), 65 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java diff --git a/build.gradle b/build.gradle index 69eee6e5..d16fd3b8 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { compile 'de.timroes.android:EnhancedListView:0.3.4' compile 'me.leolin:ShortcutBadger:1.1.1@aar' compile 'com.kyleduo.switchbutton:library:1.2.8' + compile 'org.whispersystems:axolotl-android:1.3.4' } android { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java new file mode 100644 index 00000000..8e300248 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -0,0 +1,440 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.util.Log; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class AxolotlService { + + private Account account; + private XmppConnectionService mXmppConnectionService; + private SQLiteAxolotlStore axolotlStore; + private Map sessions; + + public static class SQLiteAxolotlStore implements AxolotlStore { + + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "signed_prekeys"; + public static final String NAME = "name"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String ACCOUNT = "account"; + + public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + return ownKey; + } + + private static int generateRegistrationId() { + Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + int reg_id = KeyHelper.generateRegistrationId(false); + return reg_id; + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.identityKeyPair = loadIdentityKeyPair(); + this.localRegistrationId = loadRegistrationId(); + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); + IdentityKeyPair ownKey; + if( serializedKey != null ) { + try { + ownKey = new IdentityKeyPair(serializedKey.getBytes()); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); + return null; + } + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + ownKey = generateIdentityKeyPair(); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize())); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return ownKey; + } + + private int loadRegistrationId() { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return reg_id; + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + *

+ * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + *

+ * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + conversation.getContact().addAxolotlIdentityKey(identityKey, false); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + } + } + + /** + * Verify a remote client's identity key. + *

+ * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + List trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys(); + return trustedKeys.contains(identityKey); + } else { + return false; + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); + return false; + } + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + *

+ * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session!=null)?session:new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name,0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + new AxolotlAddress(name,0)); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord!"); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord!"); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + } + + private static class XmppAxolotlSession { + private List untrustedMessages; + private AxolotlStore axolotlStore; + + public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) { + this.untrustedMessages = new ArrayList<>(); + this.axolotlStore = axolotlStore; + } + + public void trust() { + for (Message message : this.untrustedMessages) { + message.trust(); + } + this.untrustedMessages = null; + } + + public boolean isTrusted() { + return (this.untrustedMessages == null); + } + + public String processReceiving(XmppAxolotlMessage incomingMessage) { + return null; + } + + public XmppAxolotlMessage processSending(String outgoingMessage) { + return null; + } + } + + public AxolotlService(Account account, XmppConnectionService connectionService) { + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.sessions = new HashMap<>(); + } + + public void trustSession(Jid counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + if(session != null) { + session.trust(); + } + } + + public boolean isTrustedSession(Jid counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + return session != null && session.isTrusted(); + } + + +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java new file mode 100644 index 00000000..b11670e4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -0,0 +1,4 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class XmppAxolotlMessage { +} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index f472361f..38312566 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -254,6 +254,10 @@ public class Account extends AbstractEntity { return keys; } + public String getKey(final String name) { + return this.keys.optString(name, null); + } + public boolean setKey(final String keyName, final String keyValue) { try { this.keys.put(keyName, keyValue); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index e546f214..2f9b375d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -2,15 +2,19 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; 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; @@ -183,20 +187,22 @@ public class Contact implements ListItem, Blockable { } public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(ACCOUNT, accountUuid); - values.put(SYSTEMNAME, systemName); - values.put(SERVERNAME, serverName); - values.put(JID, jid.toString()); - values.put(OPTIONS, subscription); - values.put(SYSTEMACCOUNT, systemAccount); - values.put(PHOTOURI, photoUri); - values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar == null ? null : avatar.getFilename()); - values.put(LAST_PRESENCE, lastseen.presence); - values.put(LAST_TIME, lastseen.time); - values.put(GROUPS, groups.toString()); - return values; + synchronized (this.keys) { + final ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid.toString()); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar == null ? null : avatar.getFilename()); + values.put(LAST_PRESENCE, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + values.put(GROUPS, groups.toString()); + return values; + } } public int getSubscription() { @@ -281,63 +287,109 @@ public class Contact implements ListItem, Blockable { } public ArrayList getOtrFingerprints() { - final ArrayList fingerprints = new ArrayList(); - try { - if (this.keys.has("otr_fingerprints")) { - final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); - 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)); + synchronized (this.keys) { + final ArrayList fingerprints = new ArrayList(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + 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)); + } } } - } - } catch (final JSONException ignored) { + } catch (final JSONException ignored) { + } + return fingerprints; } - return fingerprints; } - public boolean addOtrFingerprint(String print) { - if (getOtrFingerprints().contains(print)) { - return false; + synchronized (this.keys) { + if (getOtrFingerprints().contains(print)) { + return false; + } + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + return true; + } catch (final JSONException ignored) { + return false; + } } - try { - JSONArray fingerprints; - if (!this.keys.has("otr_fingerprints")) { - fingerprints = new JSONArray(); + } + public long getPgpKeyId() { + synchronized (this.keys) { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } } else { - fingerprints = this.keys.getJSONArray("otr_fingerprints"); + return 0; } - fingerprints.put(print); - this.keys.put("otr_fingerprints", fingerprints); - return true; - } catch (final JSONException ignored) { - return false; } } - public long getPgpKeyId() { - if (this.keys.has("pgp_keyid")) { + public void setPgpKeyId(long keyId) { + synchronized (this.keys) { try { - return this.keys.getLong("pgp_keyid"); - } catch (JSONException e) { - return 0; + this.keys.put("pgp_keyid", keyId); + } catch (final JSONException ignored) { } - } else { - return 0; } } - public void setPgpKeyId(long keyId) { - try { - this.keys.put("pgp_keyid", keyId); - } catch (final JSONException ignored) { + public List getTrustedAxolotlIdentityKeys() { + synchronized (this.keys) { + JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); + List identityKeys = new ArrayList<>(); + if(serializedKeyItems != null) { + for(int i = 0; i getSubDeviceSessions(Account account, AxolotlAddress contact) { + List devices = new ArrayList<>(); + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID}; + String[] selectionArgs = {account.getUuid(), + contact.getName()}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND ", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + devices.add(cursor.getInt( + cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID))); + } + + cursor.close(); + return devices; + } + + public boolean containsSession(Account account, AxolotlAddress contact) { + Cursor cursor = getCursorForSession(account, contact); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); + values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, session.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + + public void deleteSession(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + args); + } + + public void deleteAllSessions(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), contact.getName()}; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + args); + } + + private Cursor getCursorForPreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public PreKeyRecord loadPreKey(Account account, int preKeyId) { + PreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, preKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new PreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public boolean containsPreKey(Account account, int preKeyId) { + Cursor cursor = getCursorForPreKey(account, preKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storePreKey(Account account, PreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); + } + + public void deletePreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(preKeyId)}; + db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { + SignedPreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public List loadSignedPreKeys(Account account) { + List prekeys = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid()}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + try { + prekeys.add(new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes())); + } catch (IOException ignored) { + } + } + return prekeys; + } + + public boolean containsSignedPreKey(Account account, int signedPreKeyId) { + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); + } + + public void deleteSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; + db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + args); + } } -- cgit v1.2.3 From 077932eb558853e4c3c3046c54db18c530495d8c Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 29 May 2015 11:18:25 +0200 Subject: CryptoNext Menu entries added --- src/main/res/menu/encryption_choices.xml | 3 +++ src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/res/menu/encryption_choices.xml b/src/main/res/menu/encryption_choices.xml index adf0ad8d..1b3fe76e 100644 --- a/src/main/res/menu/encryption_choices.xml +++ b/src/main/res/menu/encryption_choices.xml @@ -11,6 +11,9 @@ + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index dc29ffd3..30a3e4d2 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -154,6 +154,7 @@ Plain text OTR OpenPGP + Axolotl Edit account Delete account Temporarily disable -- cgit v1.2.3 From f73aa1a2006beb741bc39026bfd10e6166d7951a Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 16:56:34 +0200 Subject: Reworked axolotl protocol layer Numerous fixes --- .../crypto/axolotl/AxolotlService.java | 310 ++++++++++++++++++--- .../crypto/axolotl/NoSessionsCreatedException.java | 4 + .../crypto/axolotl/XmppAxolotlMessage.java | 180 ++++++++++++ .../eu/siacs/conversations/entities/Account.java | 7 + .../eu/siacs/conversations/entities/Contact.java | 62 +++-- .../eu/siacs/conversations/entities/Message.java | 14 +- .../conversations/persistance/DatabaseBackend.java | 90 ++++-- .../services/XmppConnectionService.java | 7 +- 8 files changed, 579 insertions(+), 95 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java 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 8e300248..eae7a9ab 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,15 +1,29 @@ package eu.siacs.conversations.crypto.axolotl; +import android.util.Base64; import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; +import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.protocol.CiphertextMessage; +import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; +import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; @@ -17,43 +31,50 @@ import org.whispersystems.libaxolotl.util.KeyHelper; import java.util.ArrayList; 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 eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; public class AxolotlService { - private Account account; - private XmppConnectionService mXmppConnectionService; - private SQLiteAxolotlStore axolotlStore; - private Map sessions; + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { public static final String PREKEY_TABLENAME = "prekeys"; public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "signed_prekeys"; - public static final String NAME = "name"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String ACCOUNT = "account"; public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; - public static final String ACCOUNT = "account"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; private final Account account; private final XmppConnectionService mXmppConnectionService; private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; + private int currentPreKeyId = 0; private static IdentityKeyPair generateIdentityKeyPair() { @@ -75,6 +96,14 @@ public class AxolotlService { this.mXmppConnectionService = service; this.identityKeyPair = loadIdentityKeyPair(); this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for( SignedPreKeyRecord record:loadSignedPreKeys()) { + Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; } // -------------------------------------- @@ -86,21 +115,22 @@ public class AxolotlService { IdentityKeyPair ownKey; if( serializedKey != null ) { try { - ownKey = new IdentityKeyPair(serializedKey.getBytes()); + ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); + return ownKey; } catch (InvalidKeyException e) { Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); - return null; +// return null; } - } else { + } //else { Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize())); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); if(success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new key to the database!"); } - } + //} return ownKey; } @@ -122,6 +152,19 @@ public class AxolotlService { return reg_id; } + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + /** * Get the local client's identity key pair. * @@ -159,7 +202,7 @@ public class AxolotlService { Jid contactJid = Jid.fromString(name); Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey, false); + conversation.getContact().addAxolotlIdentityKey(identityKey); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); } @@ -188,8 +231,8 @@ public class AxolotlService { Jid contactJid = Jid.fromString(name); Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); if (conversation != null) { - List trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys(); - return trustedKeys.contains(identityKey); + List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); } else { return false; } @@ -274,7 +317,15 @@ public class AxolotlService { @Override public void deleteAllSessions(String name) { mXmppConnectionService.databaseBackend.deleteAllSessions(account, - new AxolotlAddress(name,0)); + new AxolotlAddress(name, 0)); + } + + public boolean isTrustedSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); + } + + public void setTrustedSession(AxolotlAddress address, boolean trusted) { + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); } // -------------------------------------- @@ -292,7 +343,7 @@ public class AxolotlService { public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord!"); + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); } return record; } @@ -306,6 +357,13 @@ public class AxolotlService { @Override public void storePreKey(int preKeyId, PreKeyRecord record) { mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + } } /** @@ -342,7 +400,7 @@ public class AxolotlService { public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord!"); + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); } return record; } @@ -388,53 +446,229 @@ public class AxolotlService { } } - private static class XmppAxolotlSession { - private List untrustedMessages; - private AxolotlStore axolotlStore; - - public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) { - this.untrustedMessages = new ArrayList<>(); - this.axolotlStore = axolotlStore; + public static class XmppAxolotlSession { + private SessionCipher cipher; + private boolean isTrusted = false; + private SQLiteAxolotlStore sqLiteAxolotlStore; + private AxolotlAddress remoteAddress; + + public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); } public void trust() { - for (Message message : this.untrustedMessages) { - message.trust(); - } - this.untrustedMessages = null; + sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); + this.isTrusted = true; } public boolean isTrusted() { - return (this.untrustedMessages == null); + return this.isTrusted; } - public String processReceiving(XmppAxolotlMessage incomingMessage) { - return null; + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { + byte[] plaintext = null; + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); + Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + plaintext = cipher.decrypt(message); + } catch (InvalidMessageException|InvalidVersionException e) { + WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + } catch (LegacyMessageException|InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (DuplicateMessageException|NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + return plaintext; } - public XmppAxolotlMessage processSending(String outgoingMessage) { - return null; + public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + XmppAxolotlMessage.XmppAxolotlMessageHeader header = + new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), + ciphertextMessage.serialize()); + return header; } } + private static class AxolotlAddressMap { + protected Map> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + + } + + private static class SessionMap extends AxolotlAddressMap { + + public SessionMap(SQLiteAxolotlStore store, Account account) { + super(); + this.fillMap(store, account); + } + + private void fillMap(SQLiteAxolotlStore store, Account account) { + for(Contact contact:account.getRoster().getContacts()){ + Jid bareJid = contact.getJid().toBareJid(); + if(bareJid == null) { + continue; // FIXME: handle this? + } + String address = bareJid.toString(); + List deviceIDs = store.getSubDeviceSessions(address); + for(Integer deviceId:deviceIDs) { + AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); + this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + } + } + } + + } + + } + public AxolotlService(Account account, XmppConnectionService connectionService) { this.mXmppConnectionService = connectionService; this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.sessions = new HashMap<>(); + this.sessions = new SessionMap(axolotlStore, account); + this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } - public void trustSession(Jid counterpart) { + public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); if(session != null) { session.trust(); } } - public boolean isTrustedSession(Jid counterpart) { + public boolean isTrustedSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); return session != null && session.isTrusted(); } + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); + return ownDeviceSessions; + } + private Set findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); + return sessions; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + public int getOwnDeviceId() { + return ownDeviceId; + } + + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + } + + public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { + XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); + createSessionsIfNeeded(contact); + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + + for(XmppAxolotlSession session : findSessionsforContact(contact)) { +// if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + Log.d(Config.LOGTAG, "Building axolotl own headers..."); + for(XmppAxolotlSession session : findOwnSessions()) { + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + + return message; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + message.getSenderDeviceId()); + + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + // TODO: handle this properly + session = new XmppAxolotlSession(axolotlStore, senderAddress); + + } + + for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + if (header.getRecipientDeviceId() == ownDeviceId) { + Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + byte[] payloadKey = session.processReceiving(header); + if (payloadKey != null) { + Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + plaintextMessage = message.decrypt(session, payloadKey); + } + } + } + + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java new file mode 100644 index 00000000..663b42b5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java @@ -0,0 +1,4 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class NoSessionsCreatedException extends Throwable{ +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index b11670e4..4b87fc5c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -1,4 +1,184 @@ package eu.siacs.conversations.crypto.axolotl; +import android.util.Base64; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.InvalidKeyException; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.xml.Element; + public class XmppAxolotlMessage { + private byte[] innerKey; + private byte[] ciphertext; + private byte[] iv; + private final Set headers; + private final Contact contact; + private final int sourceDeviceId; + + public static class XmppAxolotlMessageHeader { + private final int recipientDeviceId; + private final byte[] content; + + public XmppAxolotlMessageHeader(int deviceId, byte[] content) { + this.recipientDeviceId = deviceId; + this.content = content; + } + + public XmppAxolotlMessageHeader(Element header) { + if("header".equals(header.getName())) { + this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); + this.content = Base64.decode(header.getContent(),Base64.DEFAULT); + } else { + throw new IllegalArgumentException("Argument not a

Element!"); + } + } + + public int getRecipientDeviceId() { + return recipientDeviceId; + } + + public byte[] getContents() { + return content; + } + + public Element toXml() { + Element headerElement = new Element("header"); + // TODO: generate XML + headerElement.setAttribute("rid", getRecipientDeviceId()); + headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); + return headerElement; + } + } + + public static class XmppAxolotlPlaintextMessage { + private final AxolotlService.XmppAxolotlSession session; + private final String plaintext; + + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + this.session = session; + this.plaintext = plaintext; + } + + public String getPlaintext() { + return plaintext; + } + } + + public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { + this.contact = contact; + this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); + this.headers = new HashSet<>(); + for(Element child:axolotlMessage.getChildren()) { + switch(child.getName()) { + case "header": + headers.add(new XmppAxolotlMessageHeader(child)); + break; + case "message": + iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); + ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); + break; + default: + break; + } + } + } + + public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { + this.contact = contact; + this.sourceDeviceId = sourceDeviceId; + this.headers = new HashSet<>(); + this.encrypt(plaintext); + } + + private void encrypt(String plaintext) { + try { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(128); + SecretKey secretKey = generator.generateKey(); + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + this.innerKey = secretKey.getEncoded(); + this.iv = cipher.getIV(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException e) { + + } + } + + public Contact getContact() { + return this.contact; + } + + public int getSenderDeviceId() { + return sourceDeviceId; + } + + public byte[] getCiphertext() { + return ciphertext; + } + + public Set getHeaders() { + return headers; + } + + public void addHeader(XmppAxolotlMessageHeader header) { + headers.add(header); + } + + public byte[] getInnerKey(){ + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toXml() { + // TODO: generate outer XML, add in header XML + Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); + message.setAttribute("id", sourceDeviceId); + for(XmppAxolotlMessageHeader header: headers) { + message.addChild(header.toXml()); + } + Element payload = message.addChild("message"); + payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); + payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); + return message; + } + + + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + XmppAxolotlPlaintextMessage plaintextMessage = null; + try { + + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException e) { + throw new AssertionError(e); + } + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 38312566..7a2dc3f7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OtrService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -122,6 +123,7 @@ public class Account extends AbstractEntity { protected String avatar; protected boolean online = false; private OtrService mOtrService = null; + private AxolotlService axolotlService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private String otrFingerprint; @@ -281,8 +283,13 @@ public class Account extends AbstractEntity { return values; } + public AxolotlService getAxolotlService() { + return axolotlService; + } + public void initAccountServices(final XmppConnectionService context) { this.mOtrService = new OtrService(context, this); + this.axolotlService = new AxolotlService(this, context); } public OtrService getOtrService() { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 2f9b375d..45b55e49 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.util.Base64; import android.util.Log; import org.json.JSONArray; @@ -349,20 +350,38 @@ public class Contact implements ListItem, Blockable { } } - public List getTrustedAxolotlIdentityKeys() { + public List getAxolotlIdentityKeys() { synchronized (this.keys) { JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); List identityKeys = new ArrayList<>(); + List toDelete = new ArrayList<>(); if(serializedKeyItems != null) { for(int i = 0; i= 15) { + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -547,7 +561,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - session = new SessionRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e) { throw new AssertionError(e); } @@ -590,7 +604,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, session.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); } @@ -616,6 +630,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { args); } + public boolean isTrustedSession(Account account, AxolotlAddress contact) { + boolean trusted = false; + Cursor cursor = getCursorForSession(account, contact); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + trusted = cursor.getInt(cursor.getColumnIndex( + AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; + } + cursor.close(); + return trusted; + } + + public void setTrustedSession(Account account, AxolotlAddress contact, boolean trusted) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); + values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted?1:0); + db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + private Cursor getCursorForPreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; @@ -636,7 +672,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new PreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -656,7 +692,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); } @@ -685,11 +721,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { SignedPreKeyRecord record = null; - Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -711,7 +747,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { while(cursor.moveToNext()) { try { - prekeys.add(new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes())); + prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); } catch (IOException ignored) { } } @@ -729,7 +765,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f5c54adf..bed9267b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -52,6 +52,7 @@ import de.duenndns.ssl.MemorizingTrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; @@ -273,7 +274,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } syncDirtyContacts(account); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); + account.getAxolotlService().publishOwnDeviceIdIfNeeded(); + account.getAxolotlService().publishBundleIfNeeded(); + account.getAxolotlService().publishPreKeysIfNeeded(); + + scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { resetSendingToWaiting(account); if (!account.isOptionSet(Account.OPTION_DISABLED)) { -- cgit v1.2.3 From 77619b55e47aafbe6b5a530b33ce8b6a77a615dd Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 16:58:24 +0200 Subject: Added PEP and message protocol layers Can now fetch/retrieve from PEP, as well as encode/decode messages --- .../crypto/axolotl/AxolotlService.java | 208 +++++++++++++++++++++ .../conversations/generator/AbstractGenerator.java | 4 +- .../siacs/conversations/generator/IqGenerator.java | 70 +++++++ .../conversations/generator/MessageGenerator.java | 21 +++ .../eu/siacs/conversations/parser/IqParser.java | 158 +++++++++++++++- .../siacs/conversations/parser/MessageParser.java | 46 ++++- .../java/eu/siacs/conversations/xml/Element.java | 5 + .../conversations/xmpp/stanzas/MessagePacket.java | 5 + 8 files changed, 509 insertions(+), 8 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 eae7a9ab..865f903a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -41,16 +41,26 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class AxolotlService { + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; + public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + private final Account account; private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; + private final BundleMap bundleCache; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -571,6 +581,8 @@ public class AxolotlService { } + private static class BundleMap extends AxolotlAddressMap { + } public AxolotlService(Account account, XmppConnectionService connectionService) { @@ -578,6 +590,7 @@ public class AxolotlService { this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.sessions = new SessionMap(axolotlStore, account); + this.bundleCache = new BundleMap(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } @@ -618,7 +631,202 @@ public class AxolotlService { return ownDeviceId; } + public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { + final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); + if (sessions.get(address) != null) { + return; + } + + synchronized (bundleCache) { + PreKeyBundle bundle = bundleCache.get(address); + if (bundle == null) { + bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); + bundleCache.put(address, bundle); + } + + if(bundle.getPreKey() == null) { + Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); + IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final List preKeyBundleList = parser.preKeys(packet); + if (preKeyBundleList.isEmpty()) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + return; + } + Random random = new Random(); + final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (bundle == null || newBundle == null) { + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + bundleCache.put(address, mergedBundle); + } + } + }); + } + if(bundle.getIdentityKey() == null) { + Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); + IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final PreKeyBundle newBundle = parser.bundle(packet); + if( bundle == null || newBundle == null ) { + Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), + newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), + newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); + + axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); + bundleCache.put(address, mergedBundle); + } + } + }); + } + } + } + + public void publishOwnDeviceIdIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if(deviceIds == null) { + deviceIds = new ArrayList<>(); + } + if(!deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + } + }); + } + + public void publishBundleIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + if(bundle == null) { + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + ownDeviceId); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + } + }); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + } + }); + } + + public void publishPreKeysIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + if(keys == null || keys.isEmpty()) { + Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); + List preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), 100); + for(PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( + preKeyRecords, ownDeviceId); + + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); + // TODO: implement this! + } + }); + } + } + }); + } + + + public boolean isContactAxolotlCapable(Contact contact) { + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + return sessions.hasAny(address) || bundleCache.hasAny(address); + } + + public void initiateSynchronousSession(Contact contact) { + + } + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + for(Integer deviceId: bundleCache.getAll(address).keySet()) { + Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); + AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); + if(sessions.get(remoteAddress) == null) { + Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); + SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); + try { + builder.process(bundleCache.get(remoteAddress)); + XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); + sessions.put(remoteAddress, session); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); + } catch (UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "Already have session for " + deviceId); + } + } + if(!this.hasAny(contact)) { + Log.e(Config.LOGTAG, "No Axolotl sessions available!"); + throw new NoSessionsCreatedException(); // FIXME: proper error handling + } } public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 79626511..69bb1803 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; @@ -28,7 +29,8 @@ public abstract class AbstractGenerator { "urn:xmpp:avatar:metadata+notify", "urn:xmpp:ping", "jabber:iq:version", - "http://jabber.org/protocol/chatstates"}; + "http://jabber.org/protocol/chatstates", + AxolotlService.PEP_DEVICE_LIST+"+notify"}; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 47915e3f..6daadc2a 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -1,9 +1,17 @@ package eu.siacs.conversations.generator; +import android.util.Base64; + +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + import java.util.ArrayList; import java.util.List; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; @@ -115,6 +123,68 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket retrieveDeviceIds(final Jid to) { + final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket retrieveBundleForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) { + final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket publishDeviceIds(final List ids) { + final Element item = new Element("item"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for(Integer id:ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item); + } + + public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { + final Element item = new Element("item"); + final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); + final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); + signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); + ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); + signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT)); + final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); + signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT)); + final Element identityKeyElement = bundle.addChild("identityKey"); + identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + + return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); + } + + public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { + final Element item = new Element("item"); + final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekey = prekeys.addChild("preKeyPublic"); + prekey.setAttribute("preKeyId", preKeyRecord.getId()); + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } + + return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); + } + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element query = packet.query("urn:xmpp:mam:0"); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index bc1148d9..e6032e0c 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.generator; +import android.util.Log; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -7,6 +9,11 @@ import java.util.TimeZone; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; @@ -59,6 +66,20 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } + public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + return generateAxolotlChat(message, false); + } + + public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ + MessagePacket packet = preparePacket(message, addDelay); + AxolotlService service = message.getConversation().getAccount().getAxolotlService(); + Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), + message.getBody()); + packet.setAxolotlMessage(axolotlMessage.toXml()); + return packet; + } + public MessagePacket generateOtrChat(Message message) { return generateOtrChat(message, false); } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 6039d395..00f96885 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,9 +1,19 @@ package eu.siacs.conversations.parser; +import android.util.Base64; import android.util.Log; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; + import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -60,7 +70,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public String avatarData(final IqPacket packet) { final Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); + "http://jabber.org/protocol/pubsub"); if (pubsub == null) { return null; } @@ -71,6 +81,152 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return super.avatarData(items); } + public Element getItem(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return items.findChild("item"); + } + + public List deviceIds(final Element item) { + List deviceIds = new ArrayList<>(); + if (item == null) { + return null; + } + final Element list = item.findChild("list"); + if(list == null) { + return null; + } + for(Element device : list.getChildren()) { + if(!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + + ", skipping..."); + continue; + } + } + return deviceIds; + } + + public Integer signedPreKeyId(final Element bundle) { + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); + } + + public ECPublicKey signedPreKeyPublic(final Element bundle) { + ECPublicKey publicKey = null; + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + try { + publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + } + return publicKey; + } + + public byte[] signedPreKeySignature(final Element bundle) { + final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); + if(signedPreKeySignature == null) { + return null; + } + return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT); + } + + public IdentityKey identityKey(final Element bundle) { + IdentityKey identityKey = null; + final Element identityKeyElement = bundle.findChild("identityKey"); + if(identityKeyElement == null) { + return null; + } + try { + identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG,"Invalid identityKey in PEP: "+e.getMessage()); + } + return identityKey; + } + + public Map preKeyPublics(final IqPacket packet) { + Map preKeyRecords = new HashMap<>(); + Element prekeysItem = getItem(packet); + if (prekeysItem == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + final Element prekeysElement = prekeysItem.findChild("prekeys"); + if(prekeysElement == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + for(Element preKeyPublicElement : prekeysElement.getChildren()) { + if(!preKeyPublicElement.getName().equals("preKeyPublic")){ + Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + continue; + } + Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); + try { + ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); + preKeyRecords.put(preKeyId, preKeyPublic); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); + continue; + } + } + return preKeyRecords; + } + + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if(bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if(bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if(signedPreKeyPublic == null || identityKey == null) { + return null; + } + + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } + + public List preKeys(final IqPacket preKeys) { + List bundles = new ArrayList<>(); + Map preKeyPublics = preKeyPublics(preKeys); + if ( preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } + + return bundles; + } + @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index d46ff195..bf4f3ad4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -6,7 +6,11 @@ import android.util.Pair; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import java.util.List; + import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -94,6 +98,18 @@ public class MessageParser extends AbstractParser implements } } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + } + + return finishedMessage; + } + private class Invite { Jid jid; String password; @@ -170,6 +186,18 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } + } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { + Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Element item = items.findChild("item"); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + AxolotlService axolotlService = account.getAxolotlService(); + if(account.getJid().toBareJid().equals(from)) { + } else { + Contact contact = account.getRoster().getContact(from); + for (Integer deviceId : deviceIds) { + axolotlService.fetchBundleIfNeeded(contact, deviceId); + } + } } } @@ -232,8 +260,9 @@ public class MessageParser extends AbstractParser implements timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); } final String body = packet.getBody(); - final String encrypted = packet.findChildContent("x", "jabber:x:encrypted"); final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user"); + final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); + final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -261,11 +290,11 @@ public class MessageParser extends AbstractParser implements return; } - if (extractChatState(mXmppConnectionService.find(account,from), packet)) { + if (extractChatState(mXmppConnectionService.find(account, from), packet)) { mXmppConnectionService.updateConversationUi(); } - if ((body != null || encrypted != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); if (isTypeGroupChat) { if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { @@ -294,9 +323,14 @@ public class MessageParser extends AbstractParser implements } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } - } else if (encrypted != null) { - message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status); - } else { + } else if (pgpEncrypted != null) { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); + } else if (axolotlEncrypted != null) { + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } message.setCounterpart(counterpart); diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 32657c66..dc5a68f6 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -21,6 +21,11 @@ public class Element { this.name = name; } + public Element(String name, String xmlns) { + this.name = name; + this.setAttribute("xmlns", xmlns); + } + public Element addChild(Element child) { this.content = null; children.add(child); diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index e32811af..628f0d93 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -29,6 +29,11 @@ public class MessagePacket extends AbstractStanza { this.children.add(0, body); } + public void setAxolotlMessage(Element axolotlMessage) { + this.children.remove(findChild("body")); + this.children.add(0, axolotlMessage); + } + public void setType(int type) { switch (type) { case TYPE_CHAT: -- cgit v1.2.3 From 065519d3f39f0a7b925157b50e4435957f63d8ae Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 17:01:42 +0200 Subject: Added axolotl activation code to UI --- .../services/XmppConnectionService.java | 9 +++++++++ .../conversations/ui/ContactDetailsActivity.java | 20 +++++++++++++++++++ .../conversations/ui/ConversationActivity.java | 18 +++++++++++++++++ .../conversations/ui/ConversationFragment.java | 10 ++++++++++ .../eu/siacs/conversations/ui/XmppActivity.java | 23 ++++++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index bed9267b..1ebe3a03 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -758,6 +758,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } break; + case Message.ENCRYPTION_AXOLOTL: + try { + packet = mMessageGenerator.generateAxolotlChat(message); + Log.d(Config.LOGTAG, "Succeeded generating axolotl chat message!"); + } catch (NoSessionsCreatedException e) { + message.setStatus(Message.STATUS_WAITING); + } + break; + } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index c190caed..fb04946a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -29,6 +29,7 @@ import android.widget.QuickContactBadge; import android.widget.TextView; import org.openintents.openpgp.util.OpenPgpUtils; +import org.whispersystems.libaxolotl.IdentityKey; import java.util.List; @@ -376,6 +377,25 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } }); } + for(final IdentityKey identityKey:contact.getAxolotlIdentityKeys()) { + hasKeys = true; + View view = inflater.inflate(R.layout.contact_key, keys, false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + ImageButton remove = (ImageButton) view + .findViewById(R.id.button_remove); + remove.setVisibility(View.VISIBLE); + keyType.setText("Axolotl Fingerprint"); + key.setText(identityKey.getFingerprint()); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + //confirmToDeleteFingerprint(otrFingerprint); + } + }); + } if (contact.getPgpKeyId() != 0) { hasKeys = true; View view = inflater.inflate(R.layout.contact_key, keys, false); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 96abf65b..ff1355c3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -16,6 +16,7 @@ import android.os.Bundle; import android.provider.MediaStore; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; @@ -749,6 +751,17 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } break; + case R.id.encryption_choice_axolotl: + Log.d(Config.LOGTAG, "Trying to enable axolotl..."); + if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); + conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); + item.setChecked(true); + } else { + Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); + showAxolotlNoSessionsDialog(); + } + break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); break; @@ -780,6 +793,11 @@ public class ConversationActivity extends XmppActivity case Message.ENCRYPTION_PGP: pgp.setChecked(true); break; + case Message.ENCRYPTION_AXOLOTL: + Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); + popup.getMenu().findItem(R.id.encryption_choice_axolotl) + .setChecked(true); + break; default: none.setChecked(true); break; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index d254ece7..ec50ea54 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -303,6 +303,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa sendOtrMessage(message); } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { sendPgpMessage(message); + } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { + sendAxolotlMessage(message); } else { sendPlainTextMessage(message); } @@ -1120,6 +1122,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa builder.create().show(); } + protected void sendAxolotlMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + //message.setCounterpart(conversation.getNextCounterpart()); + xmppService.sendMessage(message); + messageSent(); + } + protected void sendOtrMessage(final Message message) { final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7c994c31..eebeb040 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -266,6 +266,29 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + public void showAxolotlNoSessionsDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle("No Sessions"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Your contact is not Axolotl-capable!"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton("Foo", + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + builder.setPositiveButton("Bar", + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + builder.create().show(); + } + abstract void onBackendConnected(); protected void registerListeners() { -- cgit v1.2.3 From 299bbdf27f0144e6eed99e70a3b2e46f9a3aa301 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 26 Jun 2015 15:41:02 +0200 Subject: Reformat code to use tabs This really sucks to do it like this. Sorry. :( --- .../crypto/axolotl/AxolotlService.java | 1650 ++++++++++---------- .../crypto/axolotl/XmppAxolotlMessage.java | 320 ++-- .../eu/siacs/conversations/entities/Contact.java | 20 +- .../siacs/conversations/generator/IqGenerator.java | 36 +- .../conversations/generator/MessageGenerator.java | 22 +- .../eu/siacs/conversations/parser/IqParser.java | 76 +- .../siacs/conversations/parser/MessageParser.java | 56 +- .../conversations/persistance/DatabaseBackend.java | 34 +- .../services/XmppConnectionService.java | 6 +- .../conversations/ui/ConversationActivity.java | 10 +- 10 files changed, 1115 insertions(+), 1115 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 865f903a..5c4b34a4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -51,832 +51,832 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class AxolotlService { - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; - public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - private final SQLiteAxolotlStore axolotlStore; - private final SessionMap sessions; - private final BundleMap bundleCache; - private int ownDeviceId; - - public static class SQLiteAxolotlStore implements AxolotlStore { - - public static final String PREKEY_TABLENAME = "prekeys"; - public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "sessions"; - public static final String ACCOUNT = "account"; - public static final String DEVICE_ID = "device_id"; - public static final String ID = "id"; - public static final String KEY = "key"; - public static final String NAME = "name"; - public static final String TRUSTED = "trusted"; - - public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; - public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; - public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - - private final IdentityKeyPair identityKeyPair; - private final int localRegistrationId; - private int currentPreKeyId = 0; - - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - return ownKey; - } - - private static int generateRegistrationId() { - Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); - int reg_id = KeyHelper.generateRegistrationId(false); - return reg_id; - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.identityKeyPair = loadIdentityKeyPair(); - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - for( SignedPreKeyRecord record:loadSignedPreKeys()) { - Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); - } - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); - IdentityKeyPair ownKey; - if( serializedKey != null ) { - try { - ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); - return ownKey; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; + public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final BundleMap bundleCache; + private int ownDeviceId; + + public static class SQLiteAxolotlStore implements AxolotlStore { + + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; + + public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + private int currentPreKeyId = 0; + + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + return ownKey; + } + + private static int generateRegistrationId() { + Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + int reg_id = KeyHelper.generateRegistrationId(false); + return reg_id; + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.identityKeyPair = loadIdentityKeyPair(); + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for( SignedPreKeyRecord record:loadSignedPreKeys()) { + Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); + IdentityKeyPair ownKey; + if( serializedKey != null ) { + try { + ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); + return ownKey; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); // return null; - } - } //else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); - ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - //} - return ownKey; - } - - private int loadRegistrationId() { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int reg_id; - if (regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); - reg_id = 0; - } - return reg_id; - } - - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - *

- * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - *

- * Store a remote client's identity key as trusted. - * - * @param name The name of the remote client. - * @param identityKey The remote client's identity key. - */ - @Override - public void saveIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); - } - } - - /** - * Verify a remote client's identity key. - *

- * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param name The name of the remote client. - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); - } else { - return false; - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); - return false; - } - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - *

- * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(AxolotlAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session!=null)?session:new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name,0)); - } - - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(AxolotlAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(AxolotlAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - new AxolotlAddress(name, 0)); - } - - public boolean isTrustedSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); - } - - public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if(record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - } - - public static class XmppAxolotlSession { - private SessionCipher cipher; - private boolean isTrusted = false; - private SQLiteAxolotlStore sqLiteAxolotlStore; - private AxolotlAddress remoteAddress; - - public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { - this.cipher = new SessionCipher(store, remoteAddress); - this.remoteAddress = remoteAddress; - this.sqLiteAxolotlStore = store; - this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); - } - - public void trust() { - sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); - this.isTrusted = true; - } - - public boolean isTrusted() { - return this.isTrusted; - } - - public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { - byte[] plaintext = null; - try { - try { - PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - plaintext = cipher.decrypt(message); - } catch (InvalidMessageException|InvalidVersionException e) { - WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); - plaintext = cipher.decrypt(message); - } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } - } catch (LegacyMessageException|InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } catch (DuplicateMessageException|NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } - return plaintext; - } - - public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { - CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); - XmppAxolotlMessage.XmppAxolotlMessageHeader header = - new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), - ciphertextMessage.serialize()); - return header; - } - } - - private static class AxolotlAddressMap { - protected Map> map; - protected final Object MAP_LOCK = new Object(); - - public AxolotlAddressMap() { - this.map = new HashMap<>(); - } - - public void put(AxolotlAddress address, T value) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - devices = new HashMap<>(); - map.put(address.getName(), devices); - } - devices.put(address.getDeviceId(), value); - } - } - - public T get(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if(devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map getAll(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if(devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - - } - - private static class SessionMap extends AxolotlAddressMap { - - public SessionMap(SQLiteAxolotlStore store, Account account) { - super(); - this.fillMap(store, account); - } - - private void fillMap(SQLiteAxolotlStore store, Account account) { - for(Contact contact:account.getRoster().getContacts()){ - Jid bareJid = contact.getJid().toBareJid(); - if(bareJid == null) { - continue; // FIXME: handle this? - } - String address = bareJid.toString(); - List deviceIDs = store.getSubDeviceSessions(address); - for(Integer deviceId:deviceIDs) { - AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); - } - } - } - - } - - private static class BundleMap extends AxolotlAddressMap { - - } - - public AxolotlService(Account account, XmppConnectionService connectionService) { - this.mXmppConnectionService = connectionService; - this.account = account; - this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.sessions = new SessionMap(axolotlStore, account); - this.bundleCache = new BundleMap(); - this.ownDeviceId = axolotlStore.getLocalRegistrationId(); - } - - public void trustSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - if(session != null) { - session.trust(); - } - } - - public boolean isTrustedSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - return session != null && session.isTrusted(); - } - - private AxolotlAddress getAddressForJid(Jid jid) { - return new AxolotlAddress(jid.toString(), 0); - } - - private Set findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid()); - Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); - return ownDeviceSessions; - } - - private Set findSessionsforContact(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); - return sessions; - } - - private boolean hasAny(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - return sessions.hasAny(contactAddress); - } - - public int getOwnDeviceId() { - return ownDeviceId; - } - - public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { - final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); - if (sessions.get(address) != null) { - return; - } - - synchronized (bundleCache) { - PreKeyBundle bundle = bundleCache.get(address); - if (bundle == null) { - bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); - bundleCache.put(address, bundle); - } - - if(bundle.getPreKey() == null) { - Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); - IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final List preKeyBundleList = parser.preKeys(packet); - if (preKeyBundleList.isEmpty()) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); - return; - } - Random random = new Random(); - final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (bundle == null || newBundle == null) { - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - bundleCache.put(address, mergedBundle); - } - } - }); - } - if(bundle.getIdentityKey() == null) { - Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); - IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final PreKeyBundle newBundle = parser.bundle(packet); - if( bundle == null || newBundle == null ) { - Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), - newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), - newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); - - axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); - bundleCache.put(address, mergedBundle); - } - } - }); - } - } - } - - public void publishOwnDeviceIdIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element item = mXmppConnectionService.getIqParser().getItem(packet); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if(deviceIds == null) { - deviceIds = new ArrayList<>(); - } - if(!deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); - deviceIds.add(getOwnDeviceId()); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - } - }); - } - } - }); - } - - public void publishBundleIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - if(bundle == null) { - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); - int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); - try { - SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( - axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - ownDeviceId); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - Log.d(Config.LOGTAG, "Published bundle, got: " + packet); - } - }); - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - } - }); - } - - public void publishPreKeysIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - if(keys == null || keys.isEmpty()) { - Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); - List preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), 100); - for(PreKeyRecord record : preKeyRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( - preKeyRecords, ownDeviceId); - - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); - // TODO: implement this! - } - }); - } - } - }); - } - - - public boolean isContactAxolotlCapable(Contact contact) { - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - return sessions.hasAny(address) || bundleCache.hasAny(address); - } - - public void initiateSynchronousSession(Contact contact) { - - } - - private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { - Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - for(Integer deviceId: bundleCache.getAll(address).keySet()) { - Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); - AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); - if(sessions.get(remoteAddress) == null) { - Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); - SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); - try { - builder.process(bundleCache.get(remoteAddress)); - XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); - sessions.put(remoteAddress, session); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); - } catch (UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "Already have session for " + deviceId); - } - } - if(!this.hasAny(contact)) { - Log.e(Config.LOGTAG, "No Axolotl sessions available!"); - throw new NoSessionsCreatedException(); // FIXME: proper error handling - } - } - - public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { - XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); - createSessionsIfNeeded(contact); - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); - - for(XmppAxolotlSession session : findSessionsforContact(contact)) { + } + } //else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + ownKey = generateIdentityKeyPair(); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + //} + return ownKey; + } + + private int loadRegistrationId() { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + *

+ * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + *

+ * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + conversation.getContact().addAxolotlIdentityKey(identityKey); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + } + } + + /** + * Verify a remote client's identity key. + *

+ * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + } else { + return false; + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); + return false; + } + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + *

+ * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session!=null)?session:new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name,0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + new AxolotlAddress(name, 0)); + } + + public boolean isTrustedSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); + } + + public void setTrustedSession(AxolotlAddress address, boolean trusted) { + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + } + + public static class XmppAxolotlSession { + private SessionCipher cipher; + private boolean isTrusted = false; + private SQLiteAxolotlStore sqLiteAxolotlStore; + private AxolotlAddress remoteAddress; + + public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); + } + + public void trust() { + sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); + this.isTrusted = true; + } + + public boolean isTrusted() { + return this.isTrusted; + } + + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { + byte[] plaintext = null; + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); + Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + plaintext = cipher.decrypt(message); + } catch (InvalidMessageException|InvalidVersionException e) { + WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + } catch (LegacyMessageException|InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (DuplicateMessageException|NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + return plaintext; + } + + public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + XmppAxolotlMessage.XmppAxolotlMessageHeader header = + new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), + ciphertextMessage.serialize()); + return header; + } + } + + private static class AxolotlAddressMap { + protected Map> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + + } + + private static class SessionMap extends AxolotlAddressMap { + + public SessionMap(SQLiteAxolotlStore store, Account account) { + super(); + this.fillMap(store, account); + } + + private void fillMap(SQLiteAxolotlStore store, Account account) { + for(Contact contact:account.getRoster().getContacts()){ + Jid bareJid = contact.getJid().toBareJid(); + if(bareJid == null) { + continue; // FIXME: handle this? + } + String address = bareJid.toString(); + List deviceIDs = store.getSubDeviceSessions(address); + for(Integer deviceId:deviceIDs) { + AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); + this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + } + } + } + + } + + private static class BundleMap extends AxolotlAddressMap { + + } + + public AxolotlService(Account account, XmppConnectionService connectionService) { + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.sessions = new SessionMap(axolotlStore, account); + this.bundleCache = new BundleMap(); + this.ownDeviceId = axolotlStore.getLocalRegistrationId(); + } + + public void trustSession(AxolotlAddress counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + if(session != null) { + session.trust(); + } + } + + public boolean isTrustedSession(AxolotlAddress counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + return session != null && session.isTrusted(); + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); + return ownDeviceSessions; + } + + private Set findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); + return sessions; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + public int getOwnDeviceId() { + return ownDeviceId; + } + + public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { + final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); + if (sessions.get(address) != null) { + return; + } + + synchronized (bundleCache) { + PreKeyBundle bundle = bundleCache.get(address); + if (bundle == null) { + bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); + bundleCache.put(address, bundle); + } + + if(bundle.getPreKey() == null) { + Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); + IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final List preKeyBundleList = parser.preKeys(packet); + if (preKeyBundleList.isEmpty()) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + return; + } + Random random = new Random(); + final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (bundle == null || newBundle == null) { + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + bundleCache.put(address, mergedBundle); + } + } + }); + } + if(bundle.getIdentityKey() == null) { + Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); + IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final PreKeyBundle newBundle = parser.bundle(packet); + if( bundle == null || newBundle == null ) { + Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), + newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), + newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); + + axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); + bundleCache.put(address, mergedBundle); + } + } + }); + } + } + } + + public void publishOwnDeviceIdIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if(deviceIds == null) { + deviceIds = new ArrayList<>(); + } + if(!deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + } + }); + } + + public void publishBundleIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + if(bundle == null) { + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + ownDeviceId); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + } + }); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + } + }); + } + + public void publishPreKeysIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + if(keys == null || keys.isEmpty()) { + Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); + List preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), 100); + for(PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( + preKeyRecords, ownDeviceId); + + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); + // TODO: implement this! + } + }); + } + } + }); + } + + + public boolean isContactAxolotlCapable(Contact contact) { + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + return sessions.hasAny(address) || bundleCache.hasAny(address); + } + + public void initiateSynchronousSession(Contact contact) { + + } + + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + for(Integer deviceId: bundleCache.getAll(address).keySet()) { + Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); + AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); + if(sessions.get(remoteAddress) == null) { + Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); + SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); + try { + builder.process(bundleCache.get(remoteAddress)); + XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); + sessions.put(remoteAddress, session); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); + } catch (UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "Already have session for " + deviceId); + } + } + if(!this.hasAny(contact)) { + Log.e(Config.LOGTAG, "No Axolotl sessions available!"); + throw new NoSessionsCreatedException(); // FIXME: proper error handling + } + } + + public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { + XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); + createSessionsIfNeeded(contact); + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + + for(XmppAxolotlSession session : findSessionsforContact(contact)) { // if(!session.isTrusted()) { - // TODO: handle this properly + // TODO: handle this properly // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); - } - Log.d(Config.LOGTAG, "Building axolotl own headers..."); - for(XmppAxolotlSession session : findOwnSessions()) { - // if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); - } - - return message; - } - - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), - message.getSenderDeviceId()); - - XmppAxolotlSession session = sessions.get(senderAddress); - if (session == null) { - Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); - // TODO: handle this properly - session = new XmppAxolotlSession(axolotlStore, senderAddress); - - } - - for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { - if (header.getRecipientDeviceId() == ownDeviceId) { - Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); - byte[] payloadKey = session.processReceiving(header); - if (payloadKey != null) { - Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); - plaintextMessage = message.decrypt(session, payloadKey); - } - } - } - - return plaintextMessage; - } + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + Log.d(Config.LOGTAG, "Building axolotl own headers..."); + for(XmppAxolotlSession session : findOwnSessions()) { + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + + return message; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + message.getSenderDeviceId()); + + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + // TODO: handle this properly + session = new XmppAxolotlSession(axolotlStore, senderAddress); + + } + + for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + if (header.getRecipientDeviceId() == ownDeviceId) { + Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + byte[] payloadKey = session.processReceiving(header); + if (payloadKey != null) { + Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + plaintextMessage = message.decrypt(session, payloadKey); + } + } + } + + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 4b87fc5c..e1b95650 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -21,164 +21,164 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.xml.Element; public class XmppAxolotlMessage { - private byte[] innerKey; - private byte[] ciphertext; - private byte[] iv; - private final Set headers; - private final Contact contact; - private final int sourceDeviceId; - - public static class XmppAxolotlMessageHeader { - private final int recipientDeviceId; - private final byte[] content; - - public XmppAxolotlMessageHeader(int deviceId, byte[] content) { - this.recipientDeviceId = deviceId; - this.content = content; - } - - public XmppAxolotlMessageHeader(Element header) { - if("header".equals(header.getName())) { - this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); - this.content = Base64.decode(header.getContent(),Base64.DEFAULT); - } else { - throw new IllegalArgumentException("Argument not a

Element!"); - } - } - - public int getRecipientDeviceId() { - return recipientDeviceId; - } - - public byte[] getContents() { - return content; - } - - public Element toXml() { - Element headerElement = new Element("header"); - // TODO: generate XML - headerElement.setAttribute("rid", getRecipientDeviceId()); - headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); - return headerElement; - } - } - - public static class XmppAxolotlPlaintextMessage { - private final AxolotlService.XmppAxolotlSession session; - private final String plaintext; - - public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { - this.session = session; - this.plaintext = plaintext; - } - - public String getPlaintext() { - return plaintext; - } - } - - public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { - this.contact = contact; - this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); - this.headers = new HashSet<>(); - for(Element child:axolotlMessage.getChildren()) { - switch(child.getName()) { - case "header": - headers.add(new XmppAxolotlMessageHeader(child)); - break; - case "message": - iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); - ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); - break; - default: - break; - } - } - } - - public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { - this.contact = contact; - this.sourceDeviceId = sourceDeviceId; - this.headers = new HashSet<>(); - this.encrypt(plaintext); - } - - private void encrypt(String plaintext) { - try { - KeyGenerator generator = KeyGenerator.getInstance("AES"); - generator.init(128); - SecretKey secretKey = generator.generateKey(); - Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - this.innerKey = secretKey.getEncoded(); - this.iv = cipher.getIV(); - this.ciphertext = cipher.doFinal(plaintext.getBytes()); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException e) { - - } - } - - public Contact getContact() { - return this.contact; - } - - public int getSenderDeviceId() { - return sourceDeviceId; - } - - public byte[] getCiphertext() { - return ciphertext; - } - - public Set getHeaders() { - return headers; - } - - public void addHeader(XmppAxolotlMessageHeader header) { - headers.add(header); - } - - public byte[] getInnerKey(){ - return innerKey; - } - - public byte[] getIV() { - return this.iv; - } - - public Element toXml() { - // TODO: generate outer XML, add in header XML - Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); - message.setAttribute("id", sourceDeviceId); - for(XmppAxolotlMessageHeader header: headers) { - message.addChild(header.toXml()); - } - Element payload = message.addChild("message"); - payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); - payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); - return message; - } - - - public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { - XmppAxolotlPlaintextMessage plaintextMessage = null; - try { - - Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - - String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); - - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException - | BadPaddingException e) { - throw new AssertionError(e); - } - return plaintextMessage; - } + private byte[] innerKey; + private byte[] ciphertext; + private byte[] iv; + private final Set headers; + private final Contact contact; + private final int sourceDeviceId; + + public static class XmppAxolotlMessageHeader { + private final int recipientDeviceId; + private final byte[] content; + + public XmppAxolotlMessageHeader(int deviceId, byte[] content) { + this.recipientDeviceId = deviceId; + this.content = content; + } + + public XmppAxolotlMessageHeader(Element header) { + if("header".equals(header.getName())) { + this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); + this.content = Base64.decode(header.getContent(),Base64.DEFAULT); + } else { + throw new IllegalArgumentException("Argument not a
Element!"); + } + } + + public int getRecipientDeviceId() { + return recipientDeviceId; + } + + public byte[] getContents() { + return content; + } + + public Element toXml() { + Element headerElement = new Element("header"); + // TODO: generate XML + headerElement.setAttribute("rid", getRecipientDeviceId()); + headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); + return headerElement; + } + } + + public static class XmppAxolotlPlaintextMessage { + private final AxolotlService.XmppAxolotlSession session; + private final String plaintext; + + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + this.session = session; + this.plaintext = plaintext; + } + + public String getPlaintext() { + return plaintext; + } + } + + public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { + this.contact = contact; + this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); + this.headers = new HashSet<>(); + for(Element child:axolotlMessage.getChildren()) { + switch(child.getName()) { + case "header": + headers.add(new XmppAxolotlMessageHeader(child)); + break; + case "message": + iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); + ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); + break; + default: + break; + } + } + } + + public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { + this.contact = contact; + this.sourceDeviceId = sourceDeviceId; + this.headers = new HashSet<>(); + this.encrypt(plaintext); + } + + private void encrypt(String plaintext) { + try { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(128); + SecretKey secretKey = generator.generateKey(); + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + this.innerKey = secretKey.getEncoded(); + this.iv = cipher.getIV(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException e) { + + } + } + + public Contact getContact() { + return this.contact; + } + + public int getSenderDeviceId() { + return sourceDeviceId; + } + + public byte[] getCiphertext() { + return ciphertext; + } + + public Set getHeaders() { + return headers; + } + + public void addHeader(XmppAxolotlMessageHeader header) { + headers.add(header); + } + + public byte[] getInnerKey(){ + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toXml() { + // TODO: generate outer XML, add in header XML + Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); + message.setAttribute("id", sourceDeviceId); + for(XmppAxolotlMessageHeader header: headers) { + message.addChild(header.toXml()); + } + Element payload = message.addChild("message"); + payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); + payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); + return message; + } + + + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + XmppAxolotlPlaintextMessage plaintextMessage = null; + try { + + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException e) { + throw new AssertionError(e); + } + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 45b55e49..240d5223 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -310,7 +310,7 @@ public class Contact implements ListItem, Blockable { synchronized (this.keys) { if (getOtrFingerprints().contains(print)) { return false; - } + } try { JSONArray fingerprints; if (!this.keys.has("otr_fingerprints")) { @@ -392,12 +392,12 @@ public class Contact implements ListItem, Blockable { public boolean addAxolotlIdentityKey(IdentityKey identityKey) { synchronized (this.keys) { if(!getAxolotlIdentityKeys().contains(identityKey)) { - JSONArray keysList; - try { - keysList = this.keys.getJSONArray("axolotl_identity_key"); - } catch (JSONException e) { - keysList = new JSONArray(); - } + JSONArray keysList; + try { + keysList = this.keys.getJSONArray("axolotl_identity_key"); + } catch (JSONException e) { + keysList = new JSONArray(); + } keysList.put(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); try { @@ -406,10 +406,10 @@ public class Contact implements ListItem, Blockable { Log.e(Config.LOGTAG, "Error adding Identity Key to Contact " + this.getJid() + ": " + e.getMessage()); return false; } - return true; + return true; } else { - return false; - } + return false; + } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 6daadc2a..5b3bde6a 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -147,16 +147,16 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket publishDeviceIds(final List ids) { - final Element item = new Element("item"); - final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); - for(Integer id:ids) { - final Element device = new Element("device"); - device.setAttribute("id", id); - list.addChild(device); - } - return publish(AxolotlService.PEP_DEVICE_LIST, item); - } + public IqPacket publishDeviceIds(final List ids) { + final Element item = new Element("item"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for(Integer id:ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item); + } public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { final Element item = new Element("item"); @@ -173,17 +173,17 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); } - public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { - final Element item = new Element("item"); - final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); - for(PreKeyRecord preKeyRecord:prekeyList) { - final Element prekey = prekeys.addChild("preKeyPublic"); + public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { + final Element item = new Element("item"); + final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekey = prekeys.addChild("preKeyPublic"); prekey.setAttribute("preKeyId", preKeyRecord.getId()); - prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); - } + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); - } + } public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index e6032e0c..0b6a7c61 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -66,19 +66,19 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ - return generateAxolotlChat(message, false); - } + public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + return generateAxolotlChat(message, false); + } public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ - MessagePacket packet = preparePacket(message, addDelay); - AxolotlService service = message.getConversation().getAccount().getAxolotlService(); - Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); - XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), - message.getBody()); - packet.setAxolotlMessage(axolotlMessage.toXml()); - return packet; - } + MessagePacket packet = preparePacket(message, addDelay); + AxolotlService service = message.getConversation().getAccount().getAxolotlService(); + Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), + message.getBody()); + packet.setAxolotlMessage(axolotlMessage.toXml()); + return packet; + } public MessagePacket generateOtrChat(Message message) { return generateOtrChat(message, false); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 00f96885..df143a41 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -70,7 +70,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public String avatarData(final IqPacket packet) { final Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); + "http://jabber.org/protocol/pubsub"); if (pubsub == null) { return null; } @@ -165,19 +165,19 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public Map preKeyPublics(final IqPacket packet) { Map preKeyRecords = new HashMap<>(); - Element prekeysItem = getItem(packet); - if (prekeysItem == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); - return null; - } - final Element prekeysElement = prekeysItem.findChild("prekeys"); - if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); - return null; - } + Element prekeysItem = getItem(packet); + if (prekeysItem == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + final Element prekeysElement = prekeysItem.findChild("prekeys"); + if(prekeysElement == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } for(Element preKeyPublicElement : prekeysElement.getChildren()) { if(!preKeyPublicElement.getName().equals("preKeyPublic")){ - Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); continue; } Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); @@ -192,37 +192,37 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return preKeyRecords; } - public PreKeyBundle bundle(final IqPacket bundle) { - Element bundleItem = getItem(bundle); - if(bundleItem == null) { - return null; - } - final Element bundleElement = bundleItem.findChild("bundle"); - if(bundleElement == null) { - return null; - } - ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); - Integer signedPreKeyId = signedPreKeyId(bundleElement); - byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); - IdentityKey identityKey = identityKey(bundleElement); - if(signedPreKeyPublic == null || identityKey == null) { - return null; - } + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if(bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if(bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if(signedPreKeyPublic == null || identityKey == null) { + return null; + } - return new PreKeyBundle(0, 0, 0, null, - signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); - } + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } public List preKeys(final IqPacket preKeys) { List bundles = new ArrayList<>(); Map preKeyPublics = preKeyPublics(preKeys); - if ( preKeyPublics != null) { - for (Integer preKeyId : preKeyPublics.keySet()) { - ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); - bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, - 0, null, null, null)); - } - } + if ( preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } return bundles; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index bf4f3ad4..de705730 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -98,17 +98,17 @@ public class MessageParser extends AbstractParser implements } } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { - Message finishedMessage = null; - AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); - if(plaintextMessage != null) { - finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); - } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + } - return finishedMessage; - } + return finishedMessage; + } private class Invite { Jid jid; @@ -187,17 +187,17 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateAccountUi(); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); - Element item = items.findChild("item"); + Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Element item = items.findChild("item"); List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - AxolotlService axolotlService = account.getAxolotlService(); - if(account.getJid().toBareJid().equals(from)) { - } else { - Contact contact = account.getRoster().getContact(from); - for (Integer deviceId : deviceIds) { - axolotlService.fetchBundleIfNeeded(contact, deviceId); - } - } + AxolotlService axolotlService = account.getAxolotlService(); + if(account.getJid().toBareJid().equals(from)) { + } else { + Contact contact = account.getRoster().getContact(from); + for (Integer deviceId : deviceIds) { + axolotlService.fetchBundleIfNeeded(contact, deviceId); + } + } } } @@ -262,7 +262,7 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); + final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -324,13 +324,13 @@ public class MessageParser extends AbstractParser implements message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } } else if (pgpEncrypted != null) { - message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - } else if (axolotlEncrypted != null) { - message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); - if (message == null) { - return; - } - } else { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); + } else if (axolotlEncrypted != null) { + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } message.setCounterpart(counterpart); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index af0e2fa8..ee6c7636 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -42,7 +42,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " @@ -75,14 +75,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.NAME + ", " + + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + + AxolotlService.SQLiteAxolotlStore.NAME + ", " + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + ") ON CONFLICT REPLACE" +");"; @@ -157,12 +157,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + Conversation.ATTRIBUTES + " TEXT"); } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } + if (oldVersion < 9 && newVersion >= 9) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_TIME + " NUMBER"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_PRESENCE + " TEXT"); + } if (oldVersion < 10 && newVersion >= 10) { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RELATIVE_FILE_PATH + " TEXT"); @@ -557,7 +557,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public SessionRecord loadSession(Account account, AxolotlAddress contact) { SessionRecord session = null; - Cursor cursor = getCursorForSession(account, contact); + Cursor cursor = getCursorForSession(account, contact); if(cursor.getCount() != 0) { cursor.moveToFirst(); try { @@ -565,8 +565,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } catch (IOException e) { throw new AssertionError(e); } - } - cursor.close(); + } + cursor.close(); return session; } @@ -635,7 +635,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { Cursor cursor = getCursorForSession(account, contact); if(cursor.getCount() != 0) { cursor.moveToFirst(); - trusted = cursor.getInt(cursor.getColumnIndex( + trusted = cursor.getInt(cursor.getColumnIndex( AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; } cursor.close(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1ebe3a03..9a4cc276 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -274,9 +274,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } syncDirtyContacts(account); - account.getAxolotlService().publishOwnDeviceIdIfNeeded(); - account.getAxolotlService().publishBundleIfNeeded(); - account.getAxolotlService().publishPreKeysIfNeeded(); + account.getAxolotlService().publishOwnDeviceIdIfNeeded(); + account.getAxolotlService().publishBundleIfNeeded(); + account.getAxolotlService().publishPreKeysIfNeeded(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index ff1355c3..e4ce4a0f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -752,16 +752,16 @@ public class ConversationActivity extends XmppActivity } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, "Trying to enable axolotl..."); + Log.d(Config.LOGTAG, "Trying to enable axolotl..."); if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { - Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); + Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); item.setChecked(true); } else { - Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); + Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); showAxolotlNoSessionsDialog(); } - break; + break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); break; @@ -794,7 +794,7 @@ public class ConversationActivity extends XmppActivity pgp.setChecked(true); break; case Message.ENCRYPTION_AXOLOTL: - Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); + Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); popup.getMenu().findItem(R.id.encryption_choice_axolotl) .setChecked(true); break; -- cgit v1.2.3 From 9e07fc5651015dc60e54bcb2f796d0f932d5f925 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:40:56 +0200 Subject: DatabaseBackend bugfixes Don't leak cursors, initially create tables --- .../java/eu/siacs/conversations/persistance/DatabaseBackend.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index ee6c7636..966734ba 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -123,6 +123,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON DELETE CASCADE);"); db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); } @Override @@ -563,6 +566,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e) { + cursor.close(); throw new AssertionError(e); } } @@ -751,6 +755,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } catch (IOException ignored) { } } + cursor.close(); return prekeys; } -- cgit v1.2.3 From 74026b742bd7e1d4b79fd839f887a0beb940b0dc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:53:39 +0200 Subject: Save IdentityKeys in database --- .../crypto/axolotl/AxolotlService.java | 70 ++++++------------ .../conversations/persistance/DatabaseBackend.java | 85 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 47 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 5c4b34a4..d788ed06 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -68,12 +68,14 @@ public class AxolotlService { public static final String PREKEY_TABLENAME = "prekeys"; public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; public static final String ACCOUNT = "account"; public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; public static final String NAME = "name"; public static final String TRUSTED = "trusted"; + public static final String OWN = "ownkey"; public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; @@ -82,7 +84,7 @@ public class AxolotlService { private final Account account; private final XmppConnectionService mXmppConnectionService; - private final IdentityKeyPair identityKeyPair; + private IdentityKeyPair identityKeyPair; private final int localRegistrationId; private int currentPreKeyId = 0; @@ -104,10 +106,9 @@ public class AxolotlService { public SQLiteAxolotlStore(Account account, XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; - this.identityKeyPair = loadIdentityKeyPair(); this.localRegistrationId = loadRegistrationId(); this.currentPreKeyId = loadCurrentPreKeyId(); - for( SignedPreKeyRecord record:loadSignedPreKeys()) { + for (SignedPreKeyRecord record : loadSignedPreKeys()) { Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); } } @@ -121,26 +122,17 @@ public class AxolotlService { // -------------------------------------- private IdentityKeyPair loadIdentityKeyPair() { - String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); - IdentityKeyPair ownKey; - if( serializedKey != null ) { - try { - ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); - return ownKey; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); -// return null; - } - } //else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + String ownName = account.getJid().toBareJid().toString(); + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account, + ownName); + + if (ownKey != null) { + return ownKey; + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + ownName); ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - //} + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); + } return ownKey; } @@ -152,8 +144,8 @@ public class AxolotlService { } else { Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); - if(success) { + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new key to the database!"); @@ -182,6 +174,9 @@ public class AxolotlService { */ @Override public IdentityKeyPair getIdentityKeyPair() { + if(identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } return identityKeyPair; } @@ -208,16 +203,8 @@ public class AxolotlService { */ @Override public void saveIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); } } @@ -237,19 +224,8 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); - } else { - return false; - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); - return false; - } + Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); } // -------------------------------------- diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 966734ba..c70ffad2 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -10,13 +10,18 @@ import android.util.Base64; import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.Config; @@ -87,6 +92,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON CONFLICT REPLACE" +");"; + private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " + + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " + +");"; + private DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -126,6 +141,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL(CREATE_IDENTITIES_STATEMENT); } @Override @@ -273,6 +289,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); } } @@ -783,4 +801,71 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ID + "=?", args); } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), + name, + own?"1":"0"}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.OWN + " = ? ", + selectionArgs, + null, null, null); + + return cursor; + } + + public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) { + IdentityKeyPair identityKeyPair = null; + Cursor cursor = getIdentityKeyCursor(account, name, true); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } + cursor.close(); + + return identityKeyPair; + } + + public Set loadIdentityKeys(Account account, String name) { + Set identityKeys = new HashSet<>(); + Cursor cursor = getIdentityKeyCursor(account, name, false); + + while(cursor.moveToNext()) { + try { + identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); + } + } + cursor.close(); + + return identityKeys; + } + + private void storeIdentityKey(Account account, String name, boolean own, String base64Serialized) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); + values.put(AxolotlService.SQLiteAxolotlStore.OWN, own?1:0); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); + db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { + storeIdentityKey(account, name, false, Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + } + + public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { + storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); + } } -- cgit v1.2.3 From 6492801b89cea496b41fe77e6b2a9be18abb2081 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:55:45 +0200 Subject: Formatting fixes --- .../crypto/axolotl/AxolotlService.java | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 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 d788ed06..8879a0fe 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -248,7 +248,7 @@ public class AxolotlService { @Override public SessionRecord loadSession(AxolotlAddress address) { SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session!=null)?session:new SessionRecord(); + return (session != null) ? session : new SessionRecord(); } /** @@ -260,7 +260,7 @@ public class AxolotlService { @Override public List getSubDeviceSessions(String name) { return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name,0)); + new AxolotlAddress(name, 0)); } /** @@ -311,7 +311,7 @@ public class AxolotlService { } public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address, trusted); } // -------------------------------------- @@ -328,7 +328,7 @@ public class AxolotlService { @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if(record == null) { + if (record == null) { throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); } return record; @@ -344,8 +344,8 @@ public class AxolotlService { public void storePreKey(int preKeyId, PreKeyRecord record) { mXmppConnectionService.databaseBackend.storePreKey(account, record); currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); - if(success) { + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); @@ -385,7 +385,7 @@ public class AxolotlService { @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if(record == null) { + if (record == null) { throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); } return record; @@ -459,18 +459,18 @@ public class AxolotlService { try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); - } catch (InvalidMessageException|InvalidVersionException e) { + } catch (InvalidMessageException | InvalidVersionException e) { WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); - } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } - } catch (LegacyMessageException|InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } catch (DuplicateMessageException|NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (LegacyMessageException | InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + } catch (DuplicateMessageException | NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } return plaintext; } @@ -485,7 +485,7 @@ public class AxolotlService { } private static class AxolotlAddressMap { - protected Map> map; + protected Map> map; protected final Object MAP_LOCK = new Object(); public AxolotlAddressMap() { @@ -506,7 +506,7 @@ public class AxolotlService { public T get(AxolotlAddress address) { synchronized (MAP_LOCK) { Map devices = map.get(address.getName()); - if(devices == null) { + if (devices == null) { return null; } return devices.get(address.getDeviceId()); @@ -516,7 +516,7 @@ public class AxolotlService { public Map getAll(AxolotlAddress address) { synchronized (MAP_LOCK) { Map devices = map.get(address.getName()); - if(devices == null) { + if (devices == null) { return new HashMap<>(); } return devices; @@ -541,14 +541,14 @@ public class AxolotlService { } private void fillMap(SQLiteAxolotlStore store, Account account) { - for(Contact contact:account.getRoster().getContacts()){ + for (Contact contact : account.getRoster().getContacts()) { Jid bareJid = contact.getJid().toBareJid(); - if(bareJid == null) { + if (bareJid == null) { continue; // FIXME: handle this? } String address = bareJid.toString(); List deviceIDs = store.getSubDeviceSessions(address); - for(Integer deviceId:deviceIDs) { + for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); } @@ -572,7 +572,7 @@ public class AxolotlService { public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); - if(session != null) { + if (session != null) { session.trust(); } } -- cgit v1.2.3 From c1d23b2395bfc9570aed07ba3413f988f08d84f5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:18:11 +0200 Subject: Migrate to new PEP layout Merge prekeys into bundle node --- .../crypto/axolotl/AxolotlService.java | 252 +++++++++++---------- .../siacs/conversations/generator/IqGenerator.java | 29 +-- .../eu/siacs/conversations/parser/IqParser.java | 20 +- .../services/XmppConnectionService.java | 3 +- 4 files changed, 157 insertions(+), 147 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 8879a0fe..22e959eb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -30,6 +30,7 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.KeyHelper; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -53,14 +54,16 @@ public class AxolotlService { public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; - public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + + public static final int NUM_KEYS_TO_PUBLISH = 10; private final Account account; private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; private final BundleMap bundleCache; + private final Map> deviceIds; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -565,6 +568,7 @@ public class AxolotlService { this.mXmppConnectionService = connectionService; this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); this.bundleCache = new BundleMap(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); @@ -607,80 +611,11 @@ public class AxolotlService { return ownDeviceId; } - public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { - final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); - if (sessions.get(address) != null) { - return; - } - - synchronized (bundleCache) { - PreKeyBundle bundle = bundleCache.get(address); - if (bundle == null) { - bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); - bundleCache.put(address, bundle); - } - - if(bundle.getPreKey() == null) { - Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); - IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final List preKeyBundleList = parser.preKeys(packet); - if (preKeyBundleList.isEmpty()) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); - return; - } - Random random = new Random(); - final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (bundle == null || newBundle == null) { - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - bundleCache.put(address, mergedBundle); - } - } - }); - } - if(bundle.getIdentityKey() == null) { - Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); - IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final PreKeyBundle newBundle = parser.bundle(packet); - if( bundle == null || newBundle == null ) { - Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), - newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), - newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); - - axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); - bundleCache.put(address, mergedBundle); - } - } - }); - } + public void registerDevices(final Jid jid, final Set deviceIds) { + for(Integer i:deviceIds) { + Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } + this.deviceIds.put(jid, deviceIds); } public void publishOwnDeviceIdIfNeeded() { @@ -689,14 +624,14 @@ public class AxolotlService { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element item = mXmppConnectionService.getIqParser().getItem(packet); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if(deviceIds == null) { - deviceIds = new ArrayList<>(); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (deviceIds == null) { + deviceIds = new HashSet(); } - if(!deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + if (!deviceIds.contains(getOwnDeviceId())) { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -708,22 +643,68 @@ public class AxolotlService { }); } - public void publishBundleIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + private boolean validateBundle(PreKeyBundle bundle) { + if (bundle == null || bundle.getIdentityKey() == null + || bundle.getSignedPreKey() == null || bundle.getSignedPreKeySignature() == null) { + return false; + } + + try { + SignedPreKeyRecord signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + IdentityKey identityKey = axolotlStore.getIdentityKeyPair().getPublicKey(); + Log.d(Config.LOGTAG,"own identity key:"+identityKey.getFingerprint()+", foreign: "+bundle.getIdentityKey().getFingerprint()); + Log.d(Config.LOGTAG,"bundle: "+Boolean.toString(bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())) + +" " + Boolean.toString(Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) + +" " + Boolean.toString( bundle.getIdentityKey().equals(identityKey))); + return bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + && Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature()) + && bundle.getIdentityKey().equals(identityKey); + } catch (InvalidKeyIdException ignored) { + return false; + } + } + + private boolean validatePreKeys(Map keys) { + if(keys == null) { return false; } + for(Integer id:keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if(!preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + return false; + } + } catch (InvalidKeyIdException ignored) { + return false; + } + } + return true; + } + + public void publishBundlesIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - if(bundle == null) { - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + SignedPreKeyRecord signedPreKeyRecord; + List preKeyRecords; + if (!validateBundle(bundle) || keys.isEmpty() || !validatePreKeys(keys)) { int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); try { - SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + signedPreKeyRecord = KeyHelper.generateSignedPreKey( axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + + preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), NUM_KEYS_TO_PUBLISH); + for (PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - ownDeviceId); + preKeyRecords, ownDeviceId); + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -733,48 +714,83 @@ public class AxolotlService { }); } catch (InvalidKeyException e) { Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + return; } } } }); } - public void publishPreKeysIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - if(keys == null || keys.isEmpty()) { - Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); - List preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), 100); - for(PreKeyRecord record : preKeyRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( - preKeyRecords, ownDeviceId); - - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); - // TODO: implement this! - } - }); - } - } - }); + public boolean isContactAxolotlCapable(Contact contact) { + Jid jid = contact.getJid().toBareJid(); + AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); + return sessions.hasAny(address) || + ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } + private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { + Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId()); + + try { + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( + Jid.fromString(address.getName()), address.getDeviceId()); + Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + return; + } - public boolean isContactAxolotlCapable(Contact contact) { - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - return sessions.hasAny(address) || bundleCache.hasAny(address); - } + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey()); - public void initiateSynchronousSession(Contact contact) { + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address); + sessions.put(address, session); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + } catch (UntrustedIdentityException|InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + } + AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, + new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + processSending(message); + } + }); + } + } + }); + } catch (InvalidJidException e) { + Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName()); + } } private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5b3bde6a..0bef8853 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -10,6 +10,7 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import java.util.ArrayList; import java.util.List; +import java.util.Set; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; @@ -131,23 +132,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket retrieveBundleForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null); + public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null); if(to != null) { packet.setTo(to); } return packet; } - public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) { - final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null); - if(to != null) { - packet.setTo(to); - } - return packet; - } - - public IqPacket publishDeviceIds(final List ids) { + public IqPacket publishDeviceIds(final Set ids) { final Element item = new Element("item"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); for(Integer id:ids) { @@ -158,7 +151,8 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_DEVICE_LIST, item); } - public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + final List preKeyRecords, final int deviceId) { final Element item = new Element("item"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); @@ -170,19 +164,14 @@ public class IqGenerator extends AbstractGenerator { final Element identityKeyElement = bundle.addChild("identityKey"); identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); - return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); - } - - public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { - final Element item = new Element("item"); - final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); - for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:preKeyRecords) { final Element prekey = prekeys.addChild("preKeyPublic"); prekey.setAttribute("preKeyId", preKeyRecord.getId()); prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); } - return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); + return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item); } public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index df143a41..93551787 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -12,8 +12,10 @@ import org.whispersystems.libaxolotl.state.PreKeyBundle; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -94,8 +96,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items.findChild("item"); } - public List deviceIds(final Element item) { - List deviceIds = new ArrayList<>(); + public Set deviceIds(final Element item) { + Set deviceIds = new HashSet<>(); if (item == null) { return null; } @@ -165,14 +167,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public Map preKeyPublics(final IqPacket packet) { Map preKeyRecords = new HashMap<>(); - Element prekeysItem = getItem(packet); - if (prekeysItem == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + Element item = getItem(packet); + if (item == null) { + Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); + return null; + } + final Element bundleElement = item.findChild("bundle"); + if(bundleElement == null) { return null; } - final Element prekeysElement = prekeysItem.findChild("prekeys"); + final Element prekeysElement = bundleElement.findChild("prekeys"); if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); return null; } for(Element preKeyPublicElement : prekeysElement.getChildren()) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 9a4cc276..f96e5d7e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -275,8 +275,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } syncDirtyContacts(account); account.getAxolotlService().publishOwnDeviceIdIfNeeded(); - account.getAxolotlService().publishBundleIfNeeded(); - account.getAxolotlService().publishPreKeysIfNeeded(); + account.getAxolotlService().publishBundlesIfNeeded(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { -- cgit v1.2.3 From cb7980c65ed5d91296e3ad571298dbed434707c0 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:19:17 +0200 Subject: Use bareJid for own session retrieval --- src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 22e959eb..e4c0480a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -591,7 +591,7 @@ public class AxolotlService { } private Set findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); return ownDeviceSessions; } -- cgit v1.2.3 From 3815d4efa378846c8aef840ad659268a0bef1536 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:22:26 +0200 Subject: Fetch bundles on-demand, encrypt in background Bundles are now fetched on demand when a session needs to be established. This should lessen the chance of changes to the bundles occuring before they're used, as well as lessen the load of fetching bundles. Also, the message encryption is now done in a background thread, as this can be somewhat costly if many sessions are present. This is probably not going to be an issue in real use, but it's good practice anyway. --- .../crypto/axolotl/AxolotlService.java | 133 ++++++++++++++------- .../siacs/conversations/entities/Conversation.java | 6 +- .../conversations/generator/MessageGenerator.java | 10 +- .../siacs/conversations/parser/MessageParser.java | 12 +- .../services/XmppConnectionService.java | 13 +- 5 files changed, 109 insertions(+), 65 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 e4c0480a..54394dd2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.axolotl; +import android.support.annotation.Nullable; import android.util.Base64; import android.util.Log; @@ -42,13 +43,16 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class AxolotlService { @@ -62,8 +66,9 @@ public class AxolotlService { private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; - private final BundleMap bundleCache; private final Map> deviceIds; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -560,7 +565,13 @@ public class AxolotlService { } - private static class BundleMap extends AxolotlAddressMap { + private static enum FetchStatus { + PENDING, + SUCCESS, + ERROR + } + + private static class FetchStatusMap extends AxolotlAddressMap { } @@ -570,7 +581,8 @@ public class AxolotlService { this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); - this.bundleCache = new BundleMap(); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } @@ -793,56 +805,93 @@ public class AxolotlService { } } - private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + private boolean createSessionsIfNeeded(Conversation conversation) { + boolean newSessions = false; Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - for(Integer deviceId: bundleCache.getAll(address).keySet()) { - Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); - AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); - if(sessions.get(remoteAddress) == null) { - Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); - SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); - try { - builder.process(bundleCache.get(remoteAddress)); - XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); - sessions.put(remoteAddress, session); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); - } catch (UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "Already have session for " + deviceId); + Jid contactJid = conversation.getContact().getJid().toBareJid(); + Set addresses = new HashSet<>(); + if(deviceIds.get(contactJid) != null) { + for(Integer foreignId:this.deviceIds.get(contactJid)) { + Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId); + addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + } + } else { + Log.e(Config.LOGTAG, "Have no target devices in PEP!"); + } + Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid()); + if(deviceIds.get(account.getJid().toBareJid()) != null) { + for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { + Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId); + addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); } } - if(!this.hasAny(contact)) { - Log.e(Config.LOGTAG, "No Axolotl sessions available!"); - throw new NoSessionsCreatedException(); // FIXME: proper error handling + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + XmppAxolotlSession session = sessions.get(address); + if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(conversation, address); + newSessions = true; + } else { + Log.d(Config.LOGTAG, "Already have session for " + address.toString()); + } } + return newSessions; } - public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { - XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); - createSessionsIfNeeded(contact); - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + @Nullable + public XmppAxolotlMessage encrypt(Message message ){ + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact(), + ownDeviceId, message.getBody()); - for(XmppAxolotlSession session : findSessionsforContact(contact)) { -// if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); + if(findSessionsforContact(axolotlMessage.getContact()).isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + for (XmppAxolotlSession session : findSessionsforContact(axolotlMessage.getContact())) { + Log.d(Config.LOGTAG, session.remoteAddress.toString()); + //if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } Log.d(Config.LOGTAG, "Building axolotl own headers..."); - for(XmppAxolotlSession session : findOwnSessions()) { - // if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); + for (XmppAxolotlSession session : findOwnSessions()) { + Log.d(Config.LOGTAG, session.remoteAddress.toString()); + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } - return message; + return axolotlMessage; + } + + private void processSending(final Message message) { + executor.execute(new Runnable() { + @Override + public void run() { + MessagePacket packet = mXmppConnectionService.getMessageGenerator() + .generateAxolotlChat(message); + if (packet == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + } else { + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + mXmppConnectionService.sendMessagePacket(account, packet); + } + } + }); + } + + public void sendMessage(Message message) { + boolean newSessions = createSessionsIfNeeded(message.getConversation()); + + if (!newSessions) { + this.processSending(message); + } } public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 289ed4ea..2efd8a29 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable { } } - public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { synchronized (this.messages) { for (Message message : this.messages) { if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + && (message.getEncryption() == encryptionType)) { onMessageFound.onMessageFound(message); - } + } } } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 0b6a7c61..b0727690 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -66,16 +66,18 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + public MessagePacket generateAxolotlChat(Message message) { return generateAxolotlChat(message, false); } - public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ + public MessagePacket generateAxolotlChat(Message message, boolean addDelay) { MessagePacket packet = preparePacket(message, addDelay); AxolotlService service = message.getConversation().getAccount().getAxolotlService(); Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); - XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), - message.getBody()); + XmppAxolotlMessage axolotlMessage = service.encrypt(message); + if (axolotlMessage == null) { + return null; + } packet.setAxolotlMessage(axolotlMessage.toXml()); return packet; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index de705730..31f70b97 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -7,6 +7,7 @@ import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; import java.util.List; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -105,6 +106,7 @@ public class MessageParser extends AbstractParser implements XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + finishedMessage.setAxolotlSession(plaintextMessage.getSession()); } return finishedMessage; @@ -189,15 +191,9 @@ public class MessageParser extends AbstractParser implements } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); - if(account.getJid().toBareJid().equals(from)) { - } else { - Contact contact = account.getRoster().getContact(from); - for (Integer deviceId : deviceIds) { - axolotlService.fetchBundleIfNeeded(contact, deviceId); - } - } + axolotlService.registerDevices(from, deviceIds); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f96e5d7e..8b716121 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -703,7 +703,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, + new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { markMessage(message,Message.STATUS_SEND_FAILED); @@ -758,12 +759,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - try { - packet = mMessageGenerator.generateAxolotlChat(message); - Log.d(Config.LOGTAG, "Succeeded generating axolotl chat message!"); - } catch (NoSessionsCreatedException e) { - message.setStatus(Message.STATUS_WAITING); - } + message.setStatus(Message.STATUS_WAITING); + account.getAxolotlService().sendMessage(message); break; } @@ -1797,7 +1794,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.getJid().toBareJid() + " otr session established with " + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { -- cgit v1.2.3 From 1b0596d57473f7aafa633ed2d5f3b3610a653a51 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:25:23 +0200 Subject: Tag messages with originating session This can be used later in order to display trust status of messages, as well as for potential resending of messages in case of preKey conflicts. --- .../eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java | 5 +++++ src/main/java/eu/siacs/conversations/entities/Message.java | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index e1b95650..01b06c22 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -75,6 +75,11 @@ public class XmppAxolotlMessage { public String getPlaintext() { return plaintext; } + + public AxolotlService.XmppAxolotlSession getSession() { + return session; + } + } public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 336af972..b1dffc82 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -670,4 +670,8 @@ public class Message extends AbstractEntity { public boolean isTrusted() { return this.axolotlSession != null && this.axolotlSession.isTrusted(); } + + public void setAxolotlSession(AxolotlService.XmppAxolotlSession session) { + this.axolotlSession = session; + } } -- cgit v1.2.3 From 992cf5652e37f075bfcd9ed4f862fe407f3404dc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:30:23 +0200 Subject: When receiving, add mock session if none exists We need a session object in order to build a session from a PreKeyWhisperMessage, so add an empty one when none exists on receiving a message. Warning: this will break right now if the session can not be constructed from the received message.There will be an invalid session which will break if we try to send using it. --- src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 54394dd2..6a493e54 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -904,7 +904,7 @@ public class AxolotlService { Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); - + sessions.put(senderAddress,session); } for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { -- cgit v1.2.3 From 9a0232f7e7271209afb04395091c6ca7016fed09 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:33:43 +0200 Subject: Formatting fixes --- .../java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 6a493e54..1b591f78 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -901,13 +901,13 @@ public class AxolotlService { XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { - Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); sessions.put(senderAddress,session); } - for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { if (header.getRecipientDeviceId() == ownDeviceId) { Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); -- cgit v1.2.3 From 18c1e15d002f415c4449afe06e6dc80aef5aeade Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:20:27 +0200 Subject: Rework PEP content verification Now checks which part(s) are out of sync w/ local storage, and updates only those, rather than assuming the entire node corrupt and overwriting it all (especially relevant for preKey list) --- .../crypto/axolotl/AxolotlService.java | 108 ++++++++++++--------- .../siacs/conversations/generator/IqGenerator.java | 2 +- 2 files changed, 62 insertions(+), 48 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 1b591f78..470c5206 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -655,42 +655,6 @@ public class AxolotlService { }); } - private boolean validateBundle(PreKeyBundle bundle) { - if (bundle == null || bundle.getIdentityKey() == null - || bundle.getSignedPreKey() == null || bundle.getSignedPreKeySignature() == null) { - return false; - } - - try { - SignedPreKeyRecord signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - IdentityKey identityKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - Log.d(Config.LOGTAG,"own identity key:"+identityKey.getFingerprint()+", foreign: "+bundle.getIdentityKey().getFingerprint()); - Log.d(Config.LOGTAG,"bundle: "+Boolean.toString(bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())) - +" " + Boolean.toString(Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) - +" " + Boolean.toString( bundle.getIdentityKey().equals(identityKey))); - return bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - && Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature()) - && bundle.getIdentityKey().equals(identityKey); - } catch (InvalidKeyIdException ignored) { - return false; - } - } - - private boolean validatePreKeys(Map keys) { - if(keys == null) { return false; } - for(Integer id:keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if(!preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - return false; - } - } catch (InvalidKeyIdException ignored) { - return false; - } - } - return true; - } - public void publishBundlesIfNeeded() { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -698,25 +662,75 @@ public class AxolotlService { public void onIqPacketReceived(Account account, IqPacket packet) { PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - SignedPreKeyRecord signedPreKeyRecord; - List preKeyRecords; - if (!validateBundle(bundle) || keys.isEmpty() || !validatePreKeys(keys)) { + boolean flush = false; + if (bundle == null) { + Log.e(Config.LOGTAG, "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); try { - signedPreKeyRecord = KeyHelper.generateSignedPreKey( - axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if ( flush + ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } - preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), NUM_KEYS_TO_PUBLISH); - for (PreKeyRecord record : preKeyRecords) { + // Validate PreKeys + Set preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId()+1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { axolotlStore.storePreKey(record.getId(), record); } + changed = true; + Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP."); + } + + if(changed) { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, ownDeviceId); - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing: " + publish); + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -724,10 +738,10 @@ public class AxolotlService { Log.d(Config.LOGTAG, "Published bundle, got: " + packet); } }); - } catch (InvalidKeyException e) { + } + } catch (InvalidKeyException e) { Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); return; - } } } }); diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 0bef8853..19c1d4f7 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -152,7 +152,7 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, - final List preKeyRecords, final int deviceId) { + final Set preKeyRecords, final int deviceId) { final Element item = new Element("item"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); -- cgit v1.2.3 From ec6870307e0ecee8184ddfef73444290e9d15828 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:27:35 +0200 Subject: Properly track message sender Previously, the sender was assumed to be the conversation counterpart. This broke carboned own-device messages. We now track the sender properly, and also set the status (sent by one of the own devices vs received from the counterpart) accordingly. --- .../conversations/crypto/axolotl/AxolotlService.java | 8 ++++---- .../conversations/crypto/axolotl/XmppAxolotlMessage.java | 15 ++++++++------- .../java/eu/siacs/conversations/parser/MessageParser.java | 8 ++++---- 3 files changed, 16 insertions(+), 15 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 470c5206..d4089b20 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -856,14 +856,14 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact(), + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), ownDeviceId, message.getBody()); - if(findSessionsforContact(axolotlMessage.getContact()).isEmpty()) { + if(findSessionsforContact(message.getContact()).isEmpty()) { return null; } Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); - for (XmppAxolotlSession session : findSessionsforContact(axolotlMessage.getContact())) { + for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) { Log.d(Config.LOGTAG, session.remoteAddress.toString()); //if(!session.isTrusted()) { // TODO: handle this properly @@ -910,7 +910,7 @@ public class AxolotlService { public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), message.getSenderDeviceId()); XmppAxolotlSession session = sessions.get(senderAddress); diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 01b06c22..06dd2cda 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -19,13 +19,14 @@ import javax.crypto.spec.SecretKeySpec; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; public class XmppAxolotlMessage { private byte[] innerKey; private byte[] ciphertext; private byte[] iv; private final Set headers; - private final Contact contact; + private final Jid from; private final int sourceDeviceId; public static class XmppAxolotlMessageHeader { @@ -82,8 +83,8 @@ public class XmppAxolotlMessage { } - public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { - this.contact = contact; + public XmppAxolotlMessage(Jid from, Element axolotlMessage) { + this.from = from; this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); this.headers = new HashSet<>(); for(Element child:axolotlMessage.getChildren()) { @@ -101,8 +102,8 @@ public class XmppAxolotlMessage { } } - public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { - this.contact = contact; + public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) { + this.from = from; this.sourceDeviceId = sourceDeviceId; this.headers = new HashSet<>(); this.encrypt(plaintext); @@ -124,8 +125,8 @@ public class XmppAxolotlMessage { } } - public Contact getContact() { - return this.contact; + public Jid getFrom() { + return this.from; } public int getSenderDeviceId() { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 31f70b97..cc878e7f 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -99,13 +99,13 @@ public class MessageParser extends AbstractParser implements } } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) { Message finishedMessage = null; AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage); XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { - finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); finishedMessage.setAxolotlSession(plaintextMessage.getSession()); } @@ -322,7 +322,7 @@ public class MessageParser extends AbstractParser implements } else if (pgpEncrypted != null) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null) { - message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status); if (message == null) { return; } -- cgit v1.2.3 From 69600502d217334031732cef2e80ebd4461e522d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:31:14 +0200 Subject: Fix asynchronous axolotl message sending XmppConnectionService.sendMessage() now dispatches messages to the AxolotlService, where they only are prepared for sending and cached. AxolotlService now triggers a XmppConnectionService.resendMessage(), which then handles sending the cached message packet. This transparently fixes, e.g., handling of messages sent while we are offline. --- .../crypto/axolotl/AxolotlService.java | 29 +++++++++++++++++----- .../services/XmppConnectionService.java | 6 +++-- 2 files changed, 27 insertions(+), 8 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 d4089b20..d79fa1bf 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -67,6 +67,7 @@ public class AxolotlService { private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; private final Map> deviceIds; + private final Map messageCache; private final FetchStatusMap fetchStatusMap; private final SerialSingleThreadExecutor executor; private int ownDeviceId; @@ -580,6 +581,7 @@ public class AxolotlService { this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); @@ -892,20 +894,35 @@ public class AxolotlService { .generateAxolotlChat(message); if (packet == null) { mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); } else { - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - mXmppConnectionService.sendMessagePacket(account, packet); + Log.d(Config.LOGTAG, "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), packet); + mXmppConnectionService.resendMessage(message); } } }); } - public void sendMessage(Message message) { - boolean newSessions = createSessionsIfNeeded(message.getConversation()); + public void prepareMessage(Message message) { + if (!messageCache.containsKey(message.getUuid())) { + boolean newSessions = createSessionsIfNeeded(message.getConversation()); - if (!newSessions) { - this.processSending(message); + if (!newSessions) { + this.processSending(message); + } + } + } + + public MessagePacket fetchPacketFromCache(Message message) { + MessagePacket packet = messageCache.get(message.getUuid()); + if (packet != null) { + Log.d(Config.LOGTAG, "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, "Cache miss: " + message.getUuid()); } + return packet; } public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8b716121..3e443d75 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -52,7 +52,6 @@ import de.duenndns.ssl.MemorizingTrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; @@ -760,7 +759,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case Message.ENCRYPTION_AXOLOTL: message.setStatus(Message.STATUS_WAITING); - account.getAxolotlService().sendMessage(message); + packet = account.getAxolotlService().fetchPacketFromCache(message); + if (packet == null && account.isOnlineAndConnected()) { + account.getAxolotlService().prepareMessage(message); + } break; } -- cgit v1.2.3 From bf4185ac08a42e9d16bf1e2fc0126bff467a55be Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:34:34 +0200 Subject: Refresh PEP on session establish We now track preKeys used to establish incoming sessions with us. On each new established session, we remove the used prekey from PEP. We have to do this because libaxolotl-java internally clears the used preKey from its storage, so we will not be able to establish any future sessions using that key. --- .../conversations/crypto/axolotl/AxolotlService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 d79fa1bf..420c75b5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -444,6 +444,7 @@ public class AxolotlService { public static class XmppAxolotlSession { private SessionCipher cipher; private boolean isTrusted = false; + private Integer preKeyId = null; private SQLiteAxolotlStore sqLiteAxolotlStore; private AxolotlAddress remoteAddress; @@ -463,6 +464,14 @@ public class AxolotlService { return this.isTrusted; } + public Integer getPreKeyId() { + return preKeyId; + } + + public void resetPreKeyId() { + preKeyId = null; + } + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { byte[] plaintext = null; try { @@ -470,6 +479,9 @@ public class AxolotlService { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } } catch (InvalidMessageException | InvalidVersionException e) { WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); @@ -946,6 +958,12 @@ public class AxolotlService { Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); plaintextMessage = message.decrypt(session, payloadKey); } + Integer preKeyId = session.getPreKeyId(); + if (preKeyId != null) { + publishBundlesIfNeeded(); + session.resetPreKeyId(); + } + break; } } -- cgit v1.2.3 From 25450bf6d365e4bb71addd38275296575f3a5658 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:10:43 +0200 Subject: Trust all IdentityKeys The trust-on-first-use policy leads to problems when receiving messages from two different devices of a contact before sending a message to them (as their IdentityKeys will not have been added yet). Since session trust will be managed externally anyway, this change is not a security problem, and will allow us to decrypt messages from yet-untrusted sessions. --- .../java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 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 420c75b5..cdd8d85d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -233,8 +233,9 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + //Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); + //return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + return true; } // -------------------------------------- -- cgit v1.2.3 From 6867b5c3abeeb5116a2542c56a706b733fd9cbf0 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:53:34 +0200 Subject: Return empty set on invalid PEP devicelist --- .../crypto/axolotl/AxolotlService.java | 3 +- .../eu/siacs/conversations/parser/IqParser.java | 36 +++++++++++----------- 2 files changed, 20 insertions(+), 19 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 cdd8d85d..d37879c3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.axolotl; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; import android.util.Log; @@ -638,7 +639,7 @@ public class AxolotlService { return ownDeviceId; } - public void registerDevices(final Jid jid, final Set deviceIds) { + public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { for(Integer i:deviceIds) { Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 93551787..c147978e 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.parser; +import android.support.annotation.NonNull; import android.util.Base64; import android.util.Log; @@ -96,26 +97,25 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items.findChild("item"); } + @NonNull public Set deviceIds(final Element item) { Set deviceIds = new HashSet<>(); - if (item == null) { - return null; - } - final Element list = item.findChild("list"); - if(list == null) { - return null; - } - for(Element device : list.getChildren()) { - if(!device.getName().equals("device")) { - continue; - } - try { - Integer id = Integer.valueOf(device.getAttribute("id")); - deviceIds.add(id); - } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() - + ", skipping..."); - continue; + if (item != null) { + final Element list = item.findChild("list"); + if (list != null) { + for (Element device : list.getChildren()) { + if (!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + + ", skipping..."); + continue; + } + } } } return deviceIds; -- cgit v1.2.3 From 0cf64857cfa8d42b8759ca2934af91d6060c55a5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:54:28 +0200 Subject: Only cache session if successfully established When receiving a message, only remember the XmppAxolotlSession wrapper if the prospective session was actually established. This prevents us from erroneously adding empty sessions that are never established using received PreKeyWhisperMessages, which would lead to errors if we try to use them for sending. --- .../java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d37879c3..faa0e5ad 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -944,12 +944,13 @@ public class AxolotlService { AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), message.getSenderDeviceId()); + boolean newSession = false; XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); - sessions.put(senderAddress,session); + newSession = true; } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { @@ -969,6 +970,10 @@ public class AxolotlService { } } + if (newSession && plaintextMessage != null) { + sessions.put(senderAddress,session); + } + return plaintextMessage; } } -- cgit v1.2.3 From 491f623708437f418497ecace2876e9c81708e72 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:27:12 +0200 Subject: Fix displaying Contact IdentityKeys Migrate ContactDetailsActivity to use new SQL IdentityKeys storage, remove dead code from Contact class. --- .../eu/siacs/conversations/entities/Contact.java | 64 ---------------------- .../conversations/ui/ContactDetailsActivity.java | 3 +- 2 files changed, 2 insertions(+), 65 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 240d5223..fdb5f932 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -350,70 +350,6 @@ public class Contact implements ListItem, Blockable { } } - public List getAxolotlIdentityKeys() { - synchronized (this.keys) { - JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); - List identityKeys = new ArrayList<>(); - List toDelete = new ArrayList<>(); - if(serializedKeyItems != null) { - for(int i = 0; i Date: Tue, 7 Jul 2015 19:28:35 +0200 Subject: Adapt prettifyFingerprint() to axolotl FP sizes --- .../java/eu/siacs/conversations/ui/ContactDetailsActivity.java | 2 +- src/main/java/eu/siacs/conversations/utils/CryptoHelper.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 2777b814..33861f82 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -387,7 +387,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd .findViewById(R.id.button_remove); remove.setVisibility(View.VISIBLE); keyType.setText("Axolotl Fingerprint"); - key.setText(identityKey.getFingerprint()); + key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); keys.addView(view); remove.setOnClickListener(new OnClickListener() { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 2dec203d..c7c9ac42 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -96,11 +96,10 @@ public final class CryptoHelper { } else if (fingerprint.length() < 40) { return fingerprint; } - StringBuilder builder = new StringBuilder(fingerprint); - builder.insert(8, " "); - builder.insert(17, " "); - builder.insert(26, " "); - builder.insert(35, " "); + StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s","")); + for(int i=8;i Date: Tue, 7 Jul 2015 19:30:08 +0200 Subject: Refactor axolotl database recreation --- .../conversations/persistance/DatabaseBackend.java | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index c70ffad2..a3868a1d 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -283,14 +283,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); } if (oldVersion < 15 && newVersion >= 15) { - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); - db.execSQL(CREATE_IDENTITIES_STATEMENT); + recreateAxolotlDb(); } } @@ -868,4 +861,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); } + + public void recreateAxolotlDb() { + Log.d(Config.LOGTAG, ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); + } } -- cgit v1.2.3 From 968410ae33f7ed341868847f3fedbd03ebd54f2b Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:32:52 +0200 Subject: Fix devicelist update handling No longer store own device ID (so that we don't encrypt messages for ourselves), verify that own device ID is present in update list (otherwise republish), reflect update in UI. --- .../java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 5 +++++ src/main/java/eu/siacs/conversations/parser/MessageParser.java | 1 + 2 files changed, 6 insertions(+) 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 faa0e5ad..2b0954c6 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -640,10 +640,15 @@ public class AxolotlService { } public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { + if(deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); + deviceIds.remove(getOwnDeviceId()); + } for(Integer i:deviceIds) { Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } this.deviceIds.put(jid, deviceIds); + publishOwnDeviceIdIfNeeded(); } public void publishOwnDeviceIdIfNeeded() { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index cc878e7f..0dc8d521 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -194,6 +194,7 @@ public class MessageParser extends AbstractParser implements Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); + mXmppConnectionService.updateAccountUi(); } } -- cgit v1.2.3 From 7049904c326e6dabc02138fa436d209bf724e0bc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:36:22 +0200 Subject: Add basic PEP managemend UI to EditAccountActivity EditAccountActivity now show own fingerprint, and gives an option to regenerate local keying material (and wipe all sessions associated with the old keys in the process). It also now displays a list of other own devices, and gives an option to remove all but the current device. --- .../crypto/axolotl/AxolotlService.java | 33 +++++++ .../conversations/persistance/DatabaseBackend.java | 21 ++++ .../conversations/ui/EditAccountActivity.java | 109 ++++++++++++++++++++- src/main/res/layout/activity_edit_account.xml | 101 +++++++++++++++++++ src/main/res/values/strings.xml | 6 ++ 5 files changed, 266 insertions(+), 4 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 2b0954c6..ec4eb7c5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -176,6 +176,13 @@ public class AxolotlService { return reg_id; } + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } /** * Get the local client's identity key pair. @@ -602,6 +609,10 @@ public class AxolotlService { this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } + public IdentityKey getOwnPublicKey() { + return axolotlStore.getIdentityKeyPair().getPublicKey(); + } + public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); if (session != null) { @@ -635,10 +646,19 @@ public class AxolotlService { return sessions.hasAny(contactAddress); } + public void regenerateKeys() { + axolotlStore.regenerate(); + publishBundlesIfNeeded(); + } + public int getOwnDeviceId() { return ownDeviceId; } + public Set getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().toBareJid()); + } + public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { if(deviceIds.contains(getOwnDeviceId())) { Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); @@ -651,6 +671,19 @@ public class AxolotlService { publishOwnDeviceIdIfNeeded(); } + public void wipeOtherPepDevices() { + Set deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, "Wiping all other devices from Pep:" + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + public void publishOwnDeviceIdIfNeeded() { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index a3868a1d..1aa2bade 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -874,4 +874,25 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); db.execSQL(CREATE_IDENTITIES_STATEMENT); } + + public void wipeAxolotlDb(Account account) { + String accountName = account.getUuid(); + Log.d(Config.LOGTAG, ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + String[] deleteArgs= { + accountName + }; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 908c29d2..ab4dc059 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,9 +1,13 @@ package eu.siacs.conversations.ui; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; import android.app.PendingIntent; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; @@ -23,6 +27,8 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import java.util.Set; + import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -54,9 +60,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private TextView mServerInfoPep; private TextView mSessionEst; private TextView mOtrFingerprint; + private TextView mAxolotlFingerprint; + private TextView mAxolotlDevicelist; private ImageView mAvatar; private RelativeLayout mOtrFingerprintBox; + private RelativeLayout mAxolotlFingerprintBox; + private RelativeLayout mAxolotlDevicelistBox; private ImageButton mOtrFingerprintToClipboardButton; + private ImageButton mAxolotlFingerprintToClipboardButton; + private ImageButton mWipeAxolotlPepButton; + private ImageButton mRegenerateAxolotlKeyButton; private Jid jidToEdit; private Account mAccount; @@ -310,6 +323,13 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); + this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); + this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key); + this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); + this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); + this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); @@ -477,10 +497,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mServerInfoPep.setText(R.string.server_info_unavailable); } - final String fingerprint = this.mAccount.getOtrFingerprint(); - if (fingerprint != null) { + final String otrFingerprint = this.mAccount.getOtrFingerprint(); + if (otrFingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); this.mOtrFingerprintToClipboardButton .setVisibility(View.VISIBLE); this.mOtrFingerprintToClipboardButton @@ -489,7 +509,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override public void onClick(final View v) { - if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) { + if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) { Toast.makeText( EditAccountActivity.this, R.string.toast_message_otr_fingerprint, @@ -500,6 +520,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mOtrFingerprintBox.setVisibility(View.GONE); } + final Set ownDevices = this.mAccount.getAxolotlService().getOwnDeviceIds(); + if (ownDevices != null && !ownDevices.isEmpty()) { + this.mAxolotlDevicelistBox.setVisibility(View.VISIBLE); + this.mAxolotlDevicelist.setText(TextUtils.join(", ", ownDevices)); + this.mWipeAxolotlPepButton + .setVisibility(View.VISIBLE); + this.mWipeAxolotlPepButton + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showWipePepDialog(); + } + }); + } else { + this.mAxolotlDevicelistBox.setVisibility(View.GONE); + } + final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint(); + if (axolotlFingerprint != null) { + this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE); + this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint)); + this.mAxolotlFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mAxolotlFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + + if (copyTextToClipboard(axolotlFingerprint, R.string.axolotl_fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_axolotl_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + this.mRegenerateAxolotlKeyButton + .setVisibility(View.VISIBLE); + this.mRegenerateAxolotlKeyButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + showRegenerateAxolotlKeyDialog(); + } + }); + } else { + this.mAxolotlFingerprintBox.setVisibility(View.GONE); + } } else { if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); @@ -512,4 +581,36 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mStats.setVisibility(View.GONE); } } + + public void showRegenerateAxolotlKeyDialog() { + Builder builder = new Builder(this); + builder.setTitle("Regenerate Key"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().regenerateKeys(); + } + }); + builder.create().show(); + } + + public void showWipePepDialog() { + Builder builder = new Builder(this); + builder.setTitle("Wipe PEP"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Are you sure you want to wipe all other devices from the PEP device ID list?"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().wipeOtherPepDevices(); + } + }); + builder.create().show(); + } } diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 98de84f5..df20e6f2 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -342,6 +342,107 @@ android:visibility="visible" android:contentDescription="@string/copy_otr_clipboard_description"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 30a3e4d2..60ca5613 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -207,6 +207,8 @@ Reception failed Your fingerprint OTR fingerprint + Axolotl fingerprint + Other own Axolotl Devices Verify Decrypt Conferences @@ -313,6 +315,7 @@ Conference name Use room’s subject instead of JID to identify conferences OTR fingerprint copied to clipboard! + Axolotl fingerprint copied to clipboard! You are banned from this conference This conference is members only You have been kicked from this conference @@ -379,6 +382,9 @@ Reset Account avatar Copy OTR fingerprint to clipboard + Copy Axolotl fingerprint to clipboard + Copy Axolotl fingerprint to clipboard + Wipe other devices from PEP Fetching history from server No more history on server Updating… -- cgit v1.2.3 From 3458f5bb9182ca99dd800e4cde21168323260da4 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:44:24 +0200 Subject: Clean up logging Add a fixed prefix to axolotl-related log messages, set log levels sensibly. --- .../crypto/axolotl/AxolotlService.java | 115 +++++++++++---------- .../conversations/generator/MessageGenerator.java | 2 +- .../eu/siacs/conversations/parser/IqParser.java | 15 +-- .../siacs/conversations/parser/MessageParser.java | 2 +- .../conversations/persistance/DatabaseBackend.java | 8 +- 5 files changed, 75 insertions(+), 67 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 ec4eb7c5..8ac69d37 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -61,6 +61,8 @@ public class AxolotlService { public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + public static final String LOGPREFIX = "AxolotlService"; + public static final int NUM_KEYS_TO_PUBLISH = 10; private final Account account; @@ -100,7 +102,7 @@ public class AxolotlService { private static IdentityKeyPair generateIdentityKeyPair() { - Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair..."); ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), identityKeyPairKeys.getPrivateKey()); @@ -108,7 +110,7 @@ public class AxolotlService { } private static int generateRegistrationId() { - Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID..."); int reg_id = KeyHelper.generateRegistrationId(false); return reg_id; } @@ -119,7 +121,7 @@ public class AxolotlService { this.localRegistrationId = loadRegistrationId(); this.currentPreKeyId = loadCurrentPreKeyId(); for (SignedPreKeyRecord record : loadSignedPreKeys()) { - Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId()); } } @@ -139,7 +141,7 @@ public class AxolotlService { if (ownKey != null) { return ownKey; } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + ownName); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName); ownKey = generateIdentityKeyPair(); mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); } @@ -152,13 +154,13 @@ public class AxolotlService { if (regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid()); reg_id = generateRegistrationId(); boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new key to the database!"); } } return reg_id; @@ -170,7 +172,7 @@ public class AxolotlService { if (regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid()); reg_id = 0; } return reg_id; @@ -366,7 +368,7 @@ public class AxolotlService { if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new prekey id to the database!"); } } @@ -456,11 +458,13 @@ public class AxolotlService { private Integer preKeyId = null; private SQLiteAxolotlStore sqLiteAxolotlStore; private AxolotlAddress remoteAddress; + private final Account account; - public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { this.cipher = new SessionCipher(store, remoteAddress); this.remoteAddress = remoteAddress; this.sqLiteAxolotlStore = store; + this.account = account; this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); } @@ -486,21 +490,20 @@ public class AxolotlService { try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); if (message.getPreKeyId().isPresent()) { preKeyId = message.getPreKeyId().get(); } } catch (InvalidMessageException | InvalidVersionException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received"); WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } - } catch (LegacyMessageException | InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); - } catch (DuplicateMessageException | NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } return plaintext; } @@ -580,7 +583,7 @@ public class AxolotlService { List deviceIDs = store.getSubDeviceSessions(address); for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress)); } } } @@ -596,6 +599,10 @@ public class AxolotlService { private static class FetchStatusMap extends AxolotlAddressMap { } + + public static String getLogprefix(Account account) { + return LOGPREFIX+" ("+account.getJid().toBareJid().toString()+"): "; + } public AxolotlService(Account account, XmppConnectionService connectionService) { this.mXmppConnectionService = connectionService; @@ -661,11 +668,11 @@ public class AxolotlService { public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { if(deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); deviceIds.remove(getOwnDeviceId()); } for(Integer i:deviceIds) { - Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding Device ID:"+ jid + ":"+i); } this.deviceIds.put(jid, deviceIds); publishOwnDeviceIdIfNeeded(); @@ -675,7 +682,7 @@ public class AxolotlService { Set deviceIds = new HashSet<>(); deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, "Wiping all other devices from Pep:" + publish); + 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) { @@ -697,7 +704,7 @@ public class AxolotlService { if (!deviceIds.contains(getOwnDeviceId())) { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -718,19 +725,19 @@ public class AxolotlService { Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); boolean flush = false; if (bundle == null) { - Log.e(Config.LOGTAG, "Received invalid bundle:" + packet); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid bundle:" + packet); bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null); flush = true; } if (keys == null) { - Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid prekeys:" + packet); } try { boolean changed = false; // Validate IdentityKey IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); changed = true; } @@ -742,13 +749,13 @@ public class AxolotlService { if ( flush ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; } } catch (InvalidKeyIdException e) { - Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; @@ -776,7 +783,7 @@ public class AxolotlService { axolotlStore.storePreKey(record.getId(), record); } changed = true; - Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding " + newKeys + " new preKeys to PEP."); } @@ -784,17 +791,17 @@ public class AxolotlService { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, ownDeviceId); - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { // TODO: implement this! - Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Published bundle, got: " + packet); } }); } } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); return; } } @@ -809,21 +816,21 @@ public class AxolotlService { } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { - Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); try { IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( Jid.fromString(address.getName()), address.getDeviceId()); - Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket); mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing..."); final IqParser parser = mXmppConnectionService.getIqParser(); final List preKeyBundleList = parser.preKeys(packet); final PreKeyBundle bundle = parser.bundle(packet); if (preKeyBundleList.isEmpty() || bundle == null) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); return; } @@ -845,11 +852,11 @@ public class AxolotlService { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address); sessions.put(address, session); fetchStatusMap.put(address, FetchStatus.SUCCESS); } catch (UntrustedIdentityException|InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + address + ": " + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error building session for " + address + ": " + e.getClass().getName() + ", " + e.getMessage()); fetchStatusMap.put(address, FetchStatus.ERROR); } @@ -869,32 +876,32 @@ public class AxolotlService { } }); } catch (InvalidJidException e) { - Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName()); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got address with invalid jid: " + address.getName()); } } private boolean createSessionsIfNeeded(Conversation conversation) { boolean newSessions = false; - Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Creating axolotl sessions if needed..."); Jid contactJid = conversation.getContact().getJid().toBareJid(); Set addresses = new HashSet<>(); if(deviceIds.get(contactJid) != null) { for(Integer foreignId:this.deviceIds.get(contactJid)) { - Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+foreignId); addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); } } else { - Log.e(Config.LOGTAG, "Have no target devices in PEP!"); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Have no target devices in PEP!"); } - Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+ownId); addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); } } for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, "Processing device: " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Processing device: " + address.toString()); FetchStatus status = fetchStatusMap.get(address); XmppAxolotlSession session = sessions.get(address); if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { @@ -902,7 +909,7 @@ public class AxolotlService { this.buildSessionFromPEP(conversation, address); newSessions = true; } else { - Log.d(Config.LOGTAG, "Already have session for " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); } } return newSessions; @@ -916,18 +923,18 @@ public class AxolotlService { if(findSessionsforContact(message.getContact()).isEmpty()) { return null; } - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers..."); for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) { - Log.d(Config.LOGTAG, session.remoteAddress.toString()); + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); //if(!session.isTrusted()) { // TODO: handle this properly // continue; // } axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } - Log.d(Config.LOGTAG, "Building axolotl own headers..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers..."); for (XmppAxolotlSession session : findOwnSessions()) { - Log.d(Config.LOGTAG, session.remoteAddress.toString()); + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); // if(!session.isTrusted()) { // TODO: handle this properly // continue; @@ -948,7 +955,7 @@ public class AxolotlService { mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); //mXmppConnectionService.updateConversationUi(); } else { - Log.d(Config.LOGTAG, "Generated message, caching: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Generated message, caching: " + message.getUuid()); messageCache.put(message.getUuid(), packet); mXmppConnectionService.resendMessage(message); } @@ -969,10 +976,10 @@ public class AxolotlService { public MessagePacket fetchPacketFromCache(Message message) { MessagePacket packet = messageCache.get(message.getUuid()); if (packet != null) { - Log.d(Config.LOGTAG, "Cache hit: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache hit: " + message.getUuid()); messageCache.remove(message.getUuid()); } else { - Log.d(Config.LOGTAG, "Cache miss: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache miss: " + message.getUuid()); } return packet; } @@ -985,18 +992,18 @@ public class AxolotlService { boolean newSession = false; XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { - Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly - session = new XmppAxolotlSession(axolotlStore, senderAddress); + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); newSession = true; } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { if (header.getRecipientDeviceId() == ownDeviceId) { - Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { - Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message..."); plaintextMessage = message.decrypt(session, payloadKey); } Integer preKeyId = session.getPreKeyId(); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index b0727690..1dd21541 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -73,7 +73,7 @@ public class MessageGenerator extends AbstractGenerator { public MessagePacket generateAxolotlChat(Message message, boolean addDelay) { MessagePacket packet = preparePacket(message, addDelay); AxolotlService service = message.getConversation().getAccount().getAxolotlService(); - Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(message.getConversation().getAccount())+"Submitting message to axolotl service for send processing..."); XmppAxolotlMessage axolotlMessage = service.encrypt(message); if (axolotlMessage == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index c147978e..e74cb65c 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; @@ -111,7 +112,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { Integer id = Integer.valueOf(device.getAttribute("id")); deviceIds.add(id); } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid node in PEP:" + device.toString() + ", skipping..."); continue; } @@ -138,7 +139,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); } return publicKey; } @@ -160,7 +161,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG,"Invalid identityKey in PEP: "+e.getMessage()); + Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); } return identityKey; } @@ -169,7 +170,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { Map preKeyRecords = new HashMap<>(); Element item = getItem(packet); if (item == null) { - Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); return null; } final Element bundleElement = item.findChild("bundle"); @@ -178,12 +179,12 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } final Element prekeysElement = bundleElement.findChild("prekeys"); if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); return null; } for(Element preKeyPublicElement : prekeysElement.getChildren()) { if(!preKeyPublicElement.getName().equals("preKeyPublic")){ - Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement); continue; } Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); @@ -191,7 +192,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); preKeyRecords.put(preKeyId, preKeyPublic); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); continue; } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0dc8d521..8b5f97af 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -189,7 +189,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateAccountUi(); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 1aa2bade..e408fa7f 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -820,7 +820,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } } cursor.close(); @@ -836,7 +836,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); } } cursor.close(); @@ -863,7 +863,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void recreateAxolotlDb() { - Log.d(Config.LOGTAG, ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<"); SQLiteDatabase db = this.getWritableDatabase(); db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL(CREATE_SESSIONS_STATEMENT); @@ -877,7 +877,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void wipeAxolotlDb(Account account) { String accountName = account.getUuid(); - Log.d(Config.LOGTAG, ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); SQLiteDatabase db = this.getWritableDatabase(); String[] deleteArgs= { accountName -- cgit v1.2.3 From bd29653a20768039778f3cbf75609772fb16de4d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:45:37 +0200 Subject: Make some fields final --- .../java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 8ac69d37..664a248f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -453,11 +453,11 @@ public class AxolotlService { } public static class XmppAxolotlSession { - private SessionCipher cipher; + private final SessionCipher cipher; private boolean isTrusted = false; private Integer preKeyId = null; - private SQLiteAxolotlStore sqLiteAxolotlStore; - private AxolotlAddress remoteAddress; + private final SQLiteAxolotlStore sqLiteAxolotlStore; + private final AxolotlAddress remoteAddress; private final Account account; public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { -- cgit v1.2.3 From 540faeb54b1fb230e624573840284daabaf4919b Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:46:03 +0200 Subject: Clean up unused constant --- src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 1 - 1 file changed, 1 deletion(-) 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 664a248f..3c9584d3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -89,7 +89,6 @@ public class AxolotlService { public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; - public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; -- cgit v1.2.3 From f1d73b9d4e64150cda347223afdeaee2ddf595eb Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 18:13:49 +0200 Subject: Use full int range for device IDs --- src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3c9584d3..ef7c523d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -110,7 +110,7 @@ public class AxolotlService { private static int generateRegistrationId() { Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID..."); - int reg_id = KeyHelper.generateRegistrationId(false); + int reg_id = KeyHelper.generateRegistrationId(true); return reg_id; } -- cgit v1.2.3 From 2628662a7f374dc7cbb01869afb6f5f8fdf5f619 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 18:14:28 +0200 Subject: Display axolotl chat message hint --- src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 3 +++ src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ec50ea54..026c74ad 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -325,6 +325,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case Message.ENCRYPTION_OTR: mEditMessage.setHint(getString(R.string.send_otr_message)); break; + case Message.ENCRYPTION_AXOLOTL: + mEditMessage.setHint(getString(R.string.send_axolotl_message)); + break; case Message.ENCRYPTION_PGP: mEditMessage.setHint(getString(R.string.send_pgp_message)); break; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 60ca5613..6808f1bb 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -80,6 +80,7 @@ Choose presence to contact Send plain text message Send OTR encrypted message + Send Axolotl encrypted message Send OpenPGP encrypted message Your nickname has been changed Send unencrypted -- cgit v1.2.3 From 03614a0262639d1f16b0fd13ec2f4ee0767205c9 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:15:59 +0200 Subject: Fix getSubDeviceSessions SQL query --- src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e408fa7f..4fb01942 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -594,7 +594,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, columns, AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND ", + + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", selectionArgs, null, null, null); -- cgit v1.2.3 From 7f918542c845fc1746676143334a5c0eca0a1e76 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:18:54 +0200 Subject: Postpone initAccountService until roster loaded The AxolotlService depends on the roster being loaded when it is initialized so that it can fill its in-memory SessionMap. --- .../java/eu/siacs/conversations/services/XmppConnectionService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 3e443d75..ca62b7db 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -594,9 +594,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); - for (final Account account : this.accounts) { - account.initAccountServices(this); - } restoreFromDatabase(); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); @@ -955,6 +952,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Log.d(Config.LOGTAG,"restoring roster"); for(Account account : accounts) { databaseBackend.readRoster(account.getRoster()); + account.initAccountServices(XmppConnectionService.this); } getBitmapCache().evictAll(); Looper.prepare(); -- cgit v1.2.3 From d173913ebabbf8de2725dd296ae6428defd4b3b3 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:23:17 +0200 Subject: Overhauled Message tagging Messages are now tagged with the IdentityKey fingerprint of the originating session. IdentityKeys have one of three trust states: undecided (default), trusted, and untrusted/not yet trusted. --- .../crypto/axolotl/AxolotlService.java | 100 ++++++++++++--------- .../crypto/axolotl/XmppAxolotlMessage.java | 11 ++- .../eu/siacs/conversations/entities/Message.java | 19 ++-- .../siacs/conversations/parser/MessageParser.java | 3 +- .../conversations/persistance/DatabaseBackend.java | 93 +++++++++++-------- 5 files changed, 138 insertions(+), 88 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 ef7c523d..9c5c82ff 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -85,6 +85,7 @@ public class AxolotlService { public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; + public static final String FINGERPRINT = "fingerprint"; public static final String NAME = "name"; public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; @@ -99,6 +100,23 @@ public class AxolotlService { private final int localRegistrationId; private int currentPreKeyId = 0; + public enum Trust { + UNDECIDED, // 0 + TRUSTED, + UNTRUSTED; + + public String toString() { + switch(this){ + case UNDECIDED: + return "Trust undecided"; + case TRUSTED: + return "Trusted"; + case UNTRUSTED: + default: + return "Untrusted"; + } + } + }; private static IdentityKeyPair generateIdentityKeyPair() { Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair..."); @@ -242,11 +260,17 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - //Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); - //return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); return true; } + public Trust getFingerprintTrust(String name, String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, name, fingerprint); + } + + public void setFingerprintTrust(String name, String fingerprint, Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, name, fingerprint, trust); + } + // -------------------------------------- // SessionStore // -------------------------------------- @@ -325,14 +349,6 @@ public class AxolotlService { new AxolotlAddress(name, 0)); } - public boolean isTrustedSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); - } - - public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address, trusted); - } - // -------------------------------------- // PreKeyStore // -------------------------------------- @@ -453,27 +469,22 @@ public class AxolotlService { public static class XmppAxolotlSession { private final SessionCipher cipher; - private boolean isTrusted = false; private Integer preKeyId = null; private final SQLiteAxolotlStore sqLiteAxolotlStore; private final AxolotlAddress remoteAddress; private final Account account; + private String fingerprint = null; + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + this(account, store, remoteAddress); + this.fingerprint = fingerprint; + } public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { this.cipher = new SessionCipher(store, remoteAddress); this.remoteAddress = remoteAddress; this.sqLiteAxolotlStore = store; this.account = account; - this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); - } - - public void trust() { - sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); - this.isTrusted = true; - } - - public boolean isTrusted() { - return this.isTrusted; } public Integer getPreKeyId() { @@ -481,18 +492,29 @@ public class AxolotlService { } public void resetPreKeyId() { + preKeyId = null; } + public String getFingerprint() { + return fingerprint; + } + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { byte[] plaintext = null; try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - plaintext = cipher.decrypt(message); - if (message.getPreKeyId().isPresent()) { - preKeyId = message.getPreKeyId().get(); + 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); + } else { + this.fingerprint = fingerprint; + plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } } } catch (InvalidMessageException | InvalidVersionException e) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received"); @@ -582,7 +604,9 @@ public class AxolotlService { List deviceIDs = store.getSubDeviceSessions(address); for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress)); + 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)); } } } @@ -619,18 +643,6 @@ public class AxolotlService { return axolotlStore.getIdentityKeyPair().getPublicKey(); } - public void trustSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - if (session != null) { - session.trust(); - } - } - - public boolean isTrustedSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - return session != null && session.isTrusted(); - } - private AxolotlAddress getAddressForJid(Jid jid) { return new AxolotlAddress(jid.toString(), 0); } @@ -808,11 +820,19 @@ public class AxolotlService { } public boolean isContactAxolotlCapable(Contact contact) { + Jid jid = contact.getJid().toBareJid(); AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); return sessions.hasAny(address) || ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } + public SQLiteAxolotlStore.Trust getFingerprintTrust(String name, String fingerprint) { + return axolotlStore.getFingerprintTrust(name, fingerprint); + } + + public void setFingerprintTrust(String name, String fingerprint, SQLiteAxolotlStore.Trust trust) { + axolotlStore.setFingerprintTrust(name, fingerprint, trust); + } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); @@ -851,7 +871,7 @@ public class AxolotlService { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); sessions.put(address, session); fetchStatusMap.put(address, FetchStatus.SUCCESS); } catch (UntrustedIdentityException|InvalidKeyException e) { @@ -890,7 +910,7 @@ public class AxolotlService { addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); } } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Have no target devices in PEP!"); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { @@ -1003,7 +1023,7 @@ public class AxolotlService { byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message..."); - plaintextMessage = message.decrypt(session, payloadKey); + plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint()); } Integer preKeyId = session.getPreKeyId(); if (preKeyId != null) { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 06dd2cda..45995228 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -67,10 +67,12 @@ public class XmppAxolotlMessage { public static class XmppAxolotlPlaintextMessage { private final AxolotlService.XmppAxolotlSession session; private final String plaintext; + private final String fingerprint; - public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) { this.session = session; this.plaintext = plaintext; + this.fingerprint = fingerprint; } public String getPlaintext() { @@ -81,6 +83,9 @@ public class XmppAxolotlMessage { return session; } + public String getFingerprint() { + return fingerprint; + } } public XmppAxolotlMessage(Jid from, Element axolotlMessage) { @@ -167,7 +172,7 @@ public class XmppAxolotlMessage { } - public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) { XmppAxolotlPlaintextMessage plaintextMessage = null; try { @@ -178,7 +183,7 @@ public class XmppAxolotlMessage { cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index b1dffc82..9695e6fa 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -54,6 +54,7 @@ public class Message extends AbstractEntity { public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; public static final String ME_COMMAND = "/me "; @@ -67,7 +68,6 @@ public class Message extends AbstractEntity { protected int encryption; protected int status; protected int type; - private AxolotlService.XmppAxolotlSession axolotlSession = null; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -76,6 +76,7 @@ public class Message extends AbstractEntity { protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; + private String axolotlFingerprint = null; private Message() { @@ -97,6 +98,7 @@ public class Message extends AbstractEntity { TYPE_TEXT, null, null, + null, null); this.conversation = conversation; } @@ -104,7 +106,7 @@ public class Message extends AbstractEntity { private Message(final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId) { + final String relativeFilePath, final String serverMsgId, final String fingerprint) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -117,6 +119,7 @@ public class Message extends AbstractEntity { this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; } public static Message fromCursor(Cursor cursor) { @@ -153,7 +156,8 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(TYPE)), cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT))); } public static Message createStatusMessage(Conversation conversation, String body) { @@ -187,6 +191,7 @@ public class Message extends AbstractEntity { values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); return values; } @@ -667,11 +672,7 @@ public class Message extends AbstractEntity { public int height = 0; } - public boolean isTrusted() { - return this.axolotlSession != null && this.axolotlSession.isTrusted(); - } - - public void setAxolotlSession(AxolotlService.XmppAxolotlSession session) { - this.axolotlSession = session; + public void setAxolotlFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 8b5f97af..9cbe5b46 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -106,7 +106,8 @@ public class MessageParser extends AbstractParser implements XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); - finishedMessage.setAxolotlSession(plaintextMessage.getSession()); + finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); } return finishedMessage; diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 4fb01942..0da52010 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -82,7 +82,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " @@ -97,6 +96,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT PRIMARY KEY ON CONFLICT IGNORE, " + + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " @@ -132,6 +133,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + Message.RELATIVE_FILE_PATH + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, " + + Message.FINGERPRINT + " TEXT, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -284,6 +286,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 15 && newVersion >= 15) { recreateAxolotlDb(); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.FINGERPRINT + " TEXT"); } } @@ -645,28 +649,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { args); } - public boolean isTrustedSession(Account account, AxolotlAddress contact) { - boolean trusted = false; - Cursor cursor = getCursorForSession(account, contact); - if(cursor.getCount() != 0) { - cursor.moveToFirst(); - trusted = cursor.getInt(cursor.getColumnIndex( - AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; - } - cursor.close(); - return trusted; - } - - public void setTrustedSession(Account account, AxolotlAddress contact, boolean trusted) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); - values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted?1:0); - db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); - } - private Cursor getCursorForPreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; @@ -796,17 +778,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { } private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + return getIdentityKeyCursor(account, name, own, null); + } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own, String fingerprint) { final SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), - name, - own?"1":"0"}; + String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, + AxolotlService.SQLiteAxolotlStore.KEY}; + ArrayList selectionArgs = new ArrayList<>(4); + selectionArgs.add(account.getUuid()); + selectionArgs.add(name); + selectionArgs.add(own?"1":"0"); + String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.OWN + " = ? "; + if (fingerprint != null){ + selectionArgs.add(fingerprint); + selectionString += "AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? "; + } Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.OWN + " = ? ", - selectionArgs, + selectionString, + selectionArgs.toArray(new String[selectionArgs.size()]), null, null, null); return cursor; @@ -844,22 +837,52 @@ public class DatabaseBackend extends SQLiteOpenHelper { return identityKeys; } - private void storeIdentityKey(Account account, String name, boolean own, String base64Serialized) { + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); - values.put(AxolotlService.SQLiteAxolotlStore.OWN, own?1:0); + values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); + values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } + public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String name, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, name, false, fingerprint); + AxolotlService.SQLiteAxolotlStore.Trust trust = null; + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)); + trust = AxolotlService.SQLiteAxolotlStore.Trust.values()[trustValue]; + } + cursor.close(); + return trust; + } + + public boolean setIdentityKeyTrust(Account account, String name, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + name, + fingerprint + }; + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); + int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs); + return rows == 1; + } + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { - storeIdentityKey(account, name, false, Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); } public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT)); } public void recreateAxolotlDb() { -- cgit v1.2.3 From 23a4e1e6fada4eb5309db35efcf80d6f1f6f4d34 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:26:19 +0200 Subject: Display trust status in ContactDetailsActivity --- .../siacs/conversations/ui/ContactDetailsActivity.java | 3 +++ src/main/res/layout/contact_key.xml | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 33861f82..4ff47f9e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -383,11 +383,14 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); + TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); ImageButton remove = (ImageButton) view .findViewById(R.id.button_remove); remove.setVisibility(View.VISIBLE); + keyTrust.setVisibility(View.VISIBLE); keyType.setText("Axolotl Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); + keyTrust.setText(contact.getAccount().getAxolotlService().getFingerprintTrust(contact.getJid().toBareJid().toString(), identityKey.getFingerprint().replaceAll("\\s","")).toString()); keys.addView(view); remove.setOnClickListener(new OnClickListener() { diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 933b72b4..79b9af62 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -3,12 +3,11 @@ android:layout_width="wrap_content" android:layout_height="match_parent" > - @@ -24,8 +24,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/black54" + android:layout_alignParentLeft="true" + android:layout_below="@+id/key" android:textSize="?attr/TextSizeInfo"/> - + + + Date: Fri, 10 Jul 2015 02:18:01 +0200 Subject: Fix and expand key regeneration function Wipe session cache to prevent stale sessions being used. Wipe fetch status cache to enable recreation of sessions. Regenerate deviceId, so that foreign devices will talk to us again. --- .../crypto/axolotl/AxolotlService.java | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 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 9c5c82ff..6b6c3750 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -73,7 +73,6 @@ public class AxolotlService { private final Map messageCache; private final FetchStatusMap fetchStatusMap; private final SerialSingleThreadExecutor executor; - private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -97,7 +96,7 @@ public class AxolotlService { private final XmppConnectionService mXmppConnectionService; private IdentityKeyPair identityKeyPair; - private final int localRegistrationId; + private int localRegistrationId; private int currentPreKeyId = 0; public enum Trust { @@ -166,9 +165,13 @@ public class AxolotlService { } private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); int reg_id; - if (regIdString != null) { + if (!regenerate && regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid()); @@ -199,6 +202,7 @@ public class AxolotlService { mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); currentPreKeyId = 0; mXmppConnectionService.updateAccountUi(); } @@ -584,6 +588,9 @@ public class AxolotlService { } } + public void clear() { + map.clear(); + } } @@ -636,7 +643,6 @@ public class AxolotlService { this.sessions = new SessionMap(axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); - this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } public IdentityKey getOwnPublicKey() { @@ -666,11 +672,14 @@ public class AxolotlService { public void regenerateKeys() { axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); publishBundlesIfNeeded(); + publishOwnDeviceIdIfNeeded(); } public int getOwnDeviceId() { - return ownDeviceId; + return axolotlStore.loadRegistrationId(); } public Set getOwnDeviceIds() { @@ -728,7 +737,7 @@ public class AxolotlService { } public void publishBundlesIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -801,7 +810,7 @@ public class AxolotlService { if(changed) { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, ownDeviceId); + preKeyRecords, getOwnDeviceId()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override @@ -937,7 +946,7 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), - ownDeviceId, message.getBody()); + getOwnDeviceId(), message.getBody()); if(findSessionsforContact(message.getContact()).isEmpty()) { return null; @@ -1018,7 +1027,7 @@ public class AxolotlService { } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { - if (header.getRecipientDeviceId() == ownDeviceId) { + if (header.getRecipientDeviceId() == getOwnDeviceId()) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { -- cgit v1.2.3 From 160e4017dfbd458325b8b63cf527f190b3f5bc2d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:24:33 +0200 Subject: Fix IdentityKey storage model Added proper UNIQUE statement --- .../java/eu/siacs/conversations/persistance/DatabaseBackend.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 0da52010..0ba2876a 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -96,11 +96,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT PRIMARY KEY ON CONFLICT IGNORE, " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + + AxolotlService.SQLiteAxolotlStore.NAME + ", " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + + ") ON CONFLICT IGNORE" +");"; private DatabaseBackend(Context context) { -- cgit v1.2.3 From 31d375c2c374dc8a36539a78582f44accc13592e Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:25:28 +0200 Subject: Fix setIdentityKeyTrust update statement --- src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 0ba2876a..4c6bf221 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -874,8 +874,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", selectionArgs); return rows == 1; -- cgit v1.2.3 From 35714d3d08d287c5ded125c356835fb70ab342b7 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:36:29 +0200 Subject: Ensure that available sessions are always used Any time a new session is established, call syncRosterToDisk() to ensure that on subsequent restoreFromDatabase() calls, the roster is actually available. This is important so that initAccountServices() can properly initialize the SessionMap. This prevents a race condition where after adding a new account and initiating sessions with it, if the app is killed (e.g. by reinstall) before triggering a syncRosterToDisk(), subsequent restores will not have the roster available, leading to missing XmppAxolotlSessions in the SessionMap cache. As a result of this, a new session was initiated when sending a new message, and received messages could not be tagged with the originating session's fingerprint. As an added sanity check, go to the database to confirm no records are present before creating fresh XmppAxolotlSession objects (both in the sending and receiving case). --- .../crypto/axolotl/AxolotlService.java | 39 +++++++++++++++++----- 1 file changed, 30 insertions(+), 9 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 6b6c3750..23adbad8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -595,13 +595,17 @@ public class AxolotlService { } private static class SessionMap extends AxolotlAddressMap { + private final XmppConnectionService xmppConnectionService; + private final Account account; - public SessionMap(SQLiteAxolotlStore store, Account account) { + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { super(); - this.fillMap(store, account); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); } - private void fillMap(SQLiteAxolotlStore store, Account account) { + private void fillMap(SQLiteAxolotlStore store) { for (Contact contact : account.getRoster().getContacts()) { Jid bareJid = contact.getJid().toBareJid(); if (bareJid == null) { @@ -618,6 +622,11 @@ public class AxolotlService { } } + @Override + public void put(AxolotlAddress address, XmppAxolotlSession value) { + super.put(address, value); + xmppConnectionService.syncRosterToDisk(account); + } } private static enum FetchStatus { @@ -640,7 +649,7 @@ public class AxolotlService { this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); this.messageCache = new HashMap<>(); - this.sessions = new SessionMap(axolotlStore, account); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); } @@ -933,9 +942,16 @@ public class AxolotlService { FetchStatus status = fetchStatusMap.get(address); XmppAxolotlSession session = sessions.get(address); if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(conversation, address); - newSessions = true; + 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..."); + session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(conversation, address); + newSessions = true; + } } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); } @@ -1022,7 +1038,12 @@ public class AxolotlService { if (session == null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey(); + if ( identityKey != null ) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", "")); + } else { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } newSession = true; } @@ -1044,7 +1065,7 @@ public class AxolotlService { } if (newSession && plaintextMessage != null) { - sessions.put(senderAddress,session); + sessions.put(senderAddress, session); } return plaintextMessage; -- cgit v1.2.3 From 3d339460889644c859d932eb3f2e324bd5696707 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:56:44 +0200 Subject: Add key trust toggle to ContactDetailsActivity Can now toggle IdentityKey trust --- .../conversations/ui/ContactDetailsActivity.java | 49 ++++++++++++++++----- src/main/res/drawable-hdpi/ic_action_done.png | Bin 0 -> 1320 bytes src/main/res/drawable-hdpi/ic_done_black_24dp.png | Bin 0 -> 177 bytes src/main/res/drawable-mdpi/ic_action_done.png | Bin 0 -> 1197 bytes src/main/res/drawable-mdpi/ic_done_black_24dp.png | Bin 0 -> 130 bytes src/main/res/drawable-xhdpi/ic_action_done.png | Bin 0 -> 1546 bytes src/main/res/drawable-xhdpi/ic_done_black_24dp.png | Bin 0 -> 188 bytes .../res/drawable-xxhdpi/ic_done_black_24dp.png | Bin 0 -> 227 bytes .../res/drawable-xxxhdpi/ic_done_black_24dp.png | Bin 0 -> 277 bytes src/main/res/layout/contact_key.xml | 10 +++++ src/main/res/values-v21/themes.xml | 1 + src/main/res/values/attrs.xml | 1 + src/main/res/values/themes.xml | 1 + 13 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_action_done.png create mode 100644 src/main/res/drawable-hdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_action_done.png create mode 100644 src/main/res/drawable-mdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_action_done.png create mode 100644 src/main/res/drawable-xhdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 4ff47f9e..ebc1ae83 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,6 +35,7 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -363,13 +364,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); - ImageButton remove = (ImageButton) view + ImageButton removeButton = (ImageButton) view .findViewById(R.id.button_remove); - remove.setVisibility(View.VISIBLE); + removeButton.setVisibility(View.VISIBLE); keyType.setText("OTR Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); keys.addView(view); - remove.setOnClickListener(new OnClickListener() { + removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -384,19 +385,47 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); - ImageButton remove = (ImageButton) view + ImageButton removeButton = (ImageButton) view .findViewById(R.id.button_remove); - remove.setVisibility(View.VISIBLE); - keyTrust.setVisibility(View.VISIBLE); + ImageButton trustButton = (ImageButton) view + .findViewById(R.id.button_trust); + final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); + final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + final Jid bareJid = contactJid.toBareJid(); + AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() + .getFingerprintTrust(bareJid.toString(), fingerprint); + switch (trust) { + case TRUSTED: + removeButton.setVisibility(View.VISIBLE); + //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!"); + break; + case UNDECIDED: + case UNTRUSTED: + //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!"); + trustButton.setVisibility(View.VISIBLE); + break; + } keyType.setText("Axolotl Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); - keyTrust.setText(contact.getAccount().getAxolotlService().getFingerprintTrust(contact.getJid().toBareJid().toString(), identityKey.getFingerprint().replaceAll("\\s","")).toString()); + keyTrust.setText(trust.toString()); + keyTrust.setVisibility(View.VISIBLE); keys.addView(view); - remove.setOnClickListener(new OnClickListener() { - + removeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + refreshUi(); + xmppConnectionService.updateConversationUi(); + } + }); + trustButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - //confirmToDeleteFingerprint(otrFingerprint); + axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); + refreshUi(); + xmppConnectionService.updateConversationUi(); } }); } diff --git a/src/main/res/drawable-hdpi/ic_action_done.png b/src/main/res/drawable-hdpi/ic_action_done.png new file mode 100644 index 00000000..58bf9721 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_action_done.png differ diff --git a/src/main/res/drawable-hdpi/ic_done_black_24dp.png b/src/main/res/drawable-hdpi/ic_done_black_24dp.png new file mode 100644 index 00000000..d4c06072 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_done_black_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_action_done.png b/src/main/res/drawable-mdpi/ic_action_done.png new file mode 100644 index 00000000..cf5fab3a Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_action_done.png differ diff --git a/src/main/res/drawable-mdpi/ic_done_black_24dp.png b/src/main/res/drawable-mdpi/ic_done_black_24dp.png new file mode 100644 index 00000000..5e5e7cf2 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_done_black_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_action_done.png b/src/main/res/drawable-xhdpi/ic_action_done.png new file mode 100644 index 00000000..b8915716 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_action_done.png differ diff --git a/src/main/res/drawable-xhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png new file mode 100644 index 00000000..64a4944f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png new file mode 100644 index 00000000..c9c01741 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png new file mode 100644 index 00000000..2f6d6386 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png differ diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 79b9af62..a43f8083 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -50,4 +50,14 @@ android:src="?attr/icon_remove" android:visibility="invisible" /> + \ No newline at end of file diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index d1679f92..78deb772 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -18,6 +18,7 @@ @drawable/ic_file_download_white_24dp @drawable/ic_edit_white_24dp @drawable/ic_edit_grey600_24dp + @drawable/ic_done_black_24dp @drawable/ic_group_white_24dp @drawable/ic_add_white_24dp @drawable/ic_attach_file_white_24dp diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index e314f752..d471e54a 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -14,6 +14,7 @@ + diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 5c67203b..afdc3e80 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -18,6 +18,7 @@ @drawable/ic_action_download @drawable/ic_action_edit @drawable/ic_action_edit_dark + @drawable/ic_action_done @drawable/ic_action_group @drawable/ic_action_new -- cgit v1.2.3 From 9e8d9a64012531be8f6f72835c6f879d2a4451a1 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 03:00:40 +0200 Subject: Show trust status of messages' originating session Shade lock icon red if message was received in a session that has not been marked trusted by the user or fingerprint is unknown --- src/main/java/eu/siacs/conversations/entities/Message.java | 4 ++++ .../eu/siacs/conversations/ui/adapter/MessageAdapter.java | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 9695e6fa..ac6c6625 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -675,4 +675,8 @@ public class Message extends AbstractEntity { public void setAxolotlFingerprint(String fingerprint) { this.axolotlFingerprint = fingerprint; } + + public String getAxolotlFingerprint() { + return axolotlFingerprint; + } } 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 167f3f02..d2c75a5e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.text.Spannable; @@ -26,6 +27,7 @@ import android.widget.Toast; import java.util.List; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -154,6 +156,17 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); + if (message.getMergedStatus() == Message.STATUS_RECEIVED + && message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() + .getAccount().getAxolotlService().getFingerprintTrust( + message.getContact().getJid().toBareJid().toString(), + message.getAxolotlFingerprint()); + + if (trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { + viewHolder.indicator.setColorFilter(Color.RED); + } + } } String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), -- cgit v1.2.3 From 6c38e531284b76ee71ef33e7f76ba1d619b25cc2 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 03:02:49 +0200 Subject: Disable Axolotl option if not usable In MUCs or if contact is not axolotl capable, disable axolotl menu option --- .../conversations/ui/ConversationActivity.java | 20 +++++++++---------- .../eu/siacs/conversations/ui/XmppActivity.java | 23 ---------------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index e4ce4a0f..2e50af3b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -37,6 +37,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.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; @@ -752,15 +753,10 @@ public class ConversationActivity extends XmppActivity } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, "Trying to enable axolotl..."); - if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { - Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); - conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); - item.setChecked(true); - } else { - Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); - showAxolotlNoSessionsDialog(); - } + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) + + "Enabled axolotl for Contact " + conversation.getContact().getJid()); + conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); + item.setChecked(true); break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); @@ -776,13 +772,18 @@ public class ConversationActivity extends XmppActivity MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); 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); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setEnabled(false); + axolotl.setEnabled(false); } else { if (forceEncryption()) { none.setVisible(false); } } + if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + axolotl.setEnabled(false); + } switch (conversation.getNextEncryption(forceEncryption())) { case Message.ENCRYPTION_NONE: none.setChecked(true); @@ -794,7 +795,6 @@ public class ConversationActivity extends XmppActivity pgp.setChecked(true); break; case Message.ENCRYPTION_AXOLOTL: - Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); popup.getMenu().findItem(R.id.encryption_choice_axolotl) .setChecked(true); break; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index eebeb040..7c994c31 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -266,29 +266,6 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - public void showAxolotlNoSessionsDialog() { - Builder builder = new AlertDialog.Builder(this); - builder.setTitle("No Sessions"); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage("Your contact is not Axolotl-capable!"); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton("Foo", - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - builder.setPositiveButton("Bar", - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - builder.create().show(); - } - abstract void onBackendConnected(); protected void registerListeners() { -- cgit v1.2.3 From 3ab59c93a6d26362ab8ade6b88dadc099cc6cb07 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Thu, 9 Jul 2015 22:37:40 -0500 Subject: Add refresh icon to v21 theme --- .../eu/siacs/conversations/ui/EditAccountActivity.java | 1 - src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 508 bytes src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 356 bytes src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 644 bytes src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 882 bytes .../res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 1161 bytes src/main/res/values-v21/themes.xml | 1 + 7 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index ab4dc059..b33e909f 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; -import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.PendingIntent; import android.content.DialogInterface; diff --git a/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 00000000..51cc4dbd Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 00000000..c136c59f Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 00000000..7891efff Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 00000000..9c1e27d7 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 00000000..e44a6d28 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png differ diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index 78deb772..9e8775f9 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -21,6 +21,7 @@ @drawable/ic_done_black_24dp @drawable/ic_group_white_24dp @drawable/ic_add_white_24dp + @drawable/ic_refresh_grey600_24dp @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_delete_grey600_24dp -- cgit v1.2.3 From fca0c367576e4cd1f218c663ad2da6a1d4bfb392 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Thu, 9 Jul 2015 23:08:44 -0500 Subject: Fix copying of axolotl keys to clipboard --- src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index b33e909f..e3a43b8b 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -324,7 +324,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); - this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard); this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key); this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); -- cgit v1.2.3 From e8ec2ee628076aa43132c3214b786ddde64f93a6 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 15 Jul 2015 15:45:12 +0200 Subject: Don't merge messages with different trust statuses --- src/main/java/eu/siacs/conversations/entities/Message.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index ac6c6625..698775c8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -399,7 +399,8 @@ public class Message extends AbstractEntity { !message.getBody().startsWith(ME_COMMAND) && !this.getBody().startsWith(ME_COMMAND) && !this.bodyIsHeart() && - !message.bodyIsHeart() + !message.bodyIsHeart() && + this.isTrusted() == message.isTrusted() ); } @@ -679,4 +680,9 @@ public class Message extends AbstractEntity { public String getAxolotlFingerprint() { return axolotlFingerprint; } + + public boolean isTrusted() { + return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint) + == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED; + } } -- cgit v1.2.3 From 4038af2f4764f1d35d82822514d65d17ab45b302 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 15 Jul 2015 16:32:42 +0200 Subject: Fix trust status for outgoing messages Tag sent messages with own fingerprint, set own fingerprint as always trusted, include own fingerprint in database trust search, explicitly reset trust colorfilter --- .../crypto/axolotl/AxolotlService.java | 16 +++++----- .../conversations/persistance/DatabaseBackend.java | 37 ++++++++++++++-------- .../services/XmppConnectionService.java | 4 +++ .../conversations/ui/ContactDetailsActivity.java | 6 ++-- .../conversations/ui/adapter/MessageAdapter.java | 8 ++--- 5 files changed, 43 insertions(+), 28 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 23adbad8..57e57f7f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -267,12 +267,12 @@ public class AxolotlService { return true; } - public Trust getFingerprintTrust(String name, String fingerprint) { - return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, name, fingerprint); + public Trust getFingerprintTrust(String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint); } - public void setFingerprintTrust(String name, String fingerprint, Trust trust) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, name, fingerprint, trust); + public void setFingerprintTrust(String fingerprint, Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust); } // -------------------------------------- @@ -844,12 +844,12 @@ public class AxolotlService { return sessions.hasAny(address) || ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } - public SQLiteAxolotlStore.Trust getFingerprintTrust(String name, String fingerprint) { - return axolotlStore.getFingerprintTrust(name, fingerprint); + public SQLiteAxolotlStore.Trust getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintTrust(fingerprint); } - public void setFingerprintTrust(String name, String fingerprint, SQLiteAxolotlStore.Trust trust) { - axolotlStore.setFingerprintTrust(name, fingerprint, trust); + public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) { + axolotlStore.setFingerprintTrust(fingerprint, trust); } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 4c6bf221..39ef5d36 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -785,20 +785,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getIdentityKeyCursor(account, name, own, null); } - private Cursor getIdentityKeyCursor(Account account, String name, boolean own, String fingerprint) { + private Cursor getIdentityKeyCursor(Account account, String fingerprint) { + return getIdentityKeyCursor(account, null, null, fingerprint); + } + + private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) { final SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, AxolotlService.SQLiteAxolotlStore.KEY}; ArrayList selectionArgs = new ArrayList<>(4); selectionArgs.add(account.getUuid()); - selectionArgs.add(name); - selectionArgs.add(own?"1":"0"); - String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.OWN + " = ? "; + String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"; + if (name != null){ + selectionArgs.add(name); + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?"; + } if (fingerprint != null){ selectionArgs.add(fingerprint); - selectionString += "AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? "; + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?"; + } + if (own != null){ + selectionArgs.add(own?"1":"0"); + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?"; } Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, columns, @@ -842,6 +850,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { } private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { + storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED); + } + + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); @@ -849,11 +861,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.ordinal()); db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } - public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String name, String fingerprint) { - Cursor cursor = getIdentityKeyCursor(account, name, false, fingerprint); + public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, fingerprint); AxolotlService.SQLiteAxolotlStore.Trust trust = null; if (cursor.getCount() > 0) { cursor.moveToFirst(); @@ -864,18 +877,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { return trust; } - public boolean setIdentityKeyTrust(Account account, String name, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { + public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { SQLiteDatabase db = this.getWritableDatabase(); String[] selectionArgs = { account.getUuid(), - name, fingerprint }; ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", selectionArgs); return rows == 1; @@ -886,7 +897,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT)); + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); } public void recreateAxolotlDb() { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ca62b7db..92e4cf35 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -759,6 +759,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa packet = account.getAxolotlService().fetchPacketFromCache(message); if (packet == null && account.isOnlineAndConnected()) { account.getAxolotlService().prepareMessage(message); + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); } break; @@ -789,6 +790,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + break; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ebc1ae83..ef99b491 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -393,7 +393,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); final Jid bareJid = contactJid.toBareJid(); AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() - .getFingerprintTrust(bareJid.toString(), fingerprint); + .getFingerprintTrust(fingerprint); switch (trust) { case TRUSTED: removeButton.setVisibility(View.VISIBLE); @@ -413,7 +413,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + axolotlService.setFingerprintTrust(fingerprint, AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); refreshUi(); xmppConnectionService.updateConversationUi(); @@ -422,7 +422,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd trustButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + axolotlService.setFingerprintTrust(fingerprint, AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); refreshUi(); xmppConnectionService.updateConversationUi(); 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 d2c75a5e..df3a391b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -156,15 +156,15 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); - if (message.getMergedStatus() == Message.STATUS_RECEIVED - && message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() .getAccount().getAxolotlService().getFingerprintTrust( - message.getContact().getJid().toBareJid().toString(), message.getAxolotlFingerprint()); - if (trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { + if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { viewHolder.indicator.setColorFilter(Color.RED); + } else { + viewHolder.indicator.clearColorFilter(); } } } -- cgit v1.2.3 From 43703870e87bfda94993439f57c7d2692aaf7783 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 18:40:27 +0200 Subject: Remove unneccessary code --- .../java/eu/siacs/conversations/services/XmppConnectionService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 92e4cf35..60815f5e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -755,9 +755,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - message.setStatus(Message.STATUS_WAITING); packet = account.getAxolotlService().fetchPacketFromCache(message); - if (packet == null && account.isOnlineAndConnected()) { + if (packet == null) { account.getAxolotlService().prepareMessage(message); message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); } -- cgit v1.2.3 From 2045a7126242ef7656a38e445b636ae87d4b763e Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 19:44:05 +0200 Subject: Handle file transmission properly in axolotl --- .../conversations/services/XmppConnectionService.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 60815f5e..08c0b3fa 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -755,10 +755,18 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - packet = account.getAxolotlService().fetchPacketFromCache(message); - if (packet == null) { - account.getAxolotlService().prepareMessage(message); - message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + if (message.needsUploading()) { + if (account.httpUploadAvailable() || message.fixCounterpart()) { + this.sendFileMessage(message); + } else { + break; + } + } else { + packet = account.getAxolotlService().fetchPacketFromCache(message); + if (packet == null) { + account.getAxolotlService().prepareMessage(message); + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + } } break; -- cgit v1.2.3 From 9c4d55f82ce50391ac09b4f7d7a0f3576c014e56 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 19:44:45 +0200 Subject: Send correct body for HTTP files When using HTTP upload to send files, take care to transmit only the URL rather than the entire body, which contains metadata. --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 57e57f7f..8358125d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -961,8 +961,14 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), - getOwnDeviceId(), message.getBody()); + getOwnDeviceId(), content); if(findSessionsforContact(message.getContact()).isEmpty()) { return null; -- cgit v1.2.3 From 6f67469bda0ffe97cb8cd8d400affed5a17c34c5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 14:09:49 +0200 Subject: Refactor trust key ui and show in account details Refactored the trust key row UI element so it can be used in multiple places. It now also uses a slider to toggle the trust state, and the redundant trust state description was removed. EditAccountActivity now shows the keys of other devices associated with that account. --- build.gradle | 1 + .../conversations/ui/ContactDetailsActivity.java | 49 +------------- .../conversations/ui/EditAccountActivity.java | 22 ++++++ .../eu/siacs/conversations/ui/XmppActivity.java | 79 ++++++++++++++++++++++ src/main/res/layout/activity_edit_account.xml | 33 ++++++++- src/main/res/layout/contact_key.xml | 20 +++--- src/main/res/values/strings.xml | 2 + src/main/res/values/styles.xml | 6 ++ 8 files changed, 154 insertions(+), 58 deletions(-) diff --git a/build.gradle b/build.gradle index d16fd3b8..5031c01f 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { compile 'me.leolin:ShortcutBadger:1.1.1@aar' compile 'com.kyleduo.switchbutton:library:1.2.8' compile 'org.whispersystems:axolotl-android:1.3.4' + compile 'com.kyleduo.switchbutton:library:1.2.8' } android { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ef99b491..e7a8ffb7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,7 +35,6 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -381,53 +380,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( contact.getAccount(), contact.getJid().toBareJid().toString())) { hasKeys = true; - View view = inflater.inflate(R.layout.contact_key, keys, false); - TextView key = (TextView) view.findViewById(R.id.key); - TextView keyType = (TextView) view.findViewById(R.id.key_type); - TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); - ImageButton removeButton = (ImageButton) view - .findViewById(R.id.button_remove); - ImageButton trustButton = (ImageButton) view - .findViewById(R.id.button_trust); - final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); - final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); - final Jid bareJid = contactJid.toBareJid(); - AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() - .getFingerprintTrust(fingerprint); - switch (trust) { - case TRUSTED: - removeButton.setVisibility(View.VISIBLE); - //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!"); - break; - case UNDECIDED: - case UNTRUSTED: - //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!"); - trustButton.setVisibility(View.VISIBLE); - break; - } - keyType.setText("Axolotl Fingerprint"); - key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); - keyTrust.setText(trust.toString()); - keyTrust.setVisibility(View.VISIBLE); - keys.addView(view); - removeButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - axolotlService.setFingerprintTrust(fingerprint, - AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); - refreshUi(); - xmppConnectionService.updateConversationUi(); - } - }); - trustButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - axolotlService.setFingerprintTrust(fingerprint, - AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); - refreshUi(); - xmppConnectionService.updateConversationUi(); - } - }); + addFingerprintRow(keys, contact.getAccount(), identityKey); } if (contact.getPgpKeyId() != 0) { hasKeys = true; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index e3a43b8b..77ca2a67 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -26,6 +26,8 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import org.whispersystems.libaxolotl.IdentityKey; + import java.util.Set; import eu.siacs.conversations.R; @@ -69,6 +71,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private ImageButton mAxolotlFingerprintToClipboardButton; private ImageButton mWipeAxolotlPepButton; private ImageButton mRegenerateAxolotlKeyButton; + private LinearLayout keys; + private LinearLayout keysCard; private Jid jidToEdit; private Account mAccount; @@ -329,6 +333,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep); + this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card); + this.keys = (LinearLayout) findViewById(R.id.other_device_keys); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); @@ -568,6 +574,22 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mAxolotlFingerprintBox.setVisibility(View.GONE); } + final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey(); + boolean hasKeys = false; + keys.removeAllViews(); + for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( + mAccount, mAccount.getJid().toBareJid().toString())) { + if(ownKey.equals(identityKey)) { + continue; + } + hasKeys = true; + addFingerprintRow(keys, mAccount, identityKey); + } + if (hasKeys) { + keysCard.setVisibility(View.VISIBLE); + } else { + keysCard.setVisibility(View.GONE); + } } else { if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7c994c31..9dfece2f 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -43,8 +43,11 @@ import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; import com.google.zxing.BarcodeFormat; @@ -53,9 +56,12 @@ import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.kyleduo.switchbutton.SwitchButton; import net.java.otr4j.session.SessionID; +import org.whispersystems.libaxolotl.IdentityKey; + import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -65,6 +71,7 @@ import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -74,6 +81,8 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.ui.widget.Switch; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -588,6 +597,76 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + protected void addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey) { + final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + final AxolotlService.SQLiteAxolotlStore.Trust trust = account.getAxolotlService() + .getFingerprintTrust(fingerprint); + addFingerprintRowWithListeners(keys, account, identityKey, trust, true, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked != (trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED)) { + account.getAxolotlService().setFingerprintTrust(fingerprint, + (isChecked) ? AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED : + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + } + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + new View.OnClickListener() { + @Override + public void onClick(View v) { + account.getAxolotlService().setFingerprintTrust(fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + } + + ); + } + + protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account, + IdentityKey identityKey, + AxolotlService.SQLiteAxolotlStore.Trust trust, + boolean showTag, + CompoundButton.OnCheckedChangeListener + onCheckedChangeListener, + View.OnClickListener onClickListener) { + 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); + Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust); + trustToggle.setVisibility(View.VISIBLE); + trustToggle.setOnCheckedChangeListener(onCheckedChangeListener); + trustToggle.setOnClickListener(onClickListener); + + switch (trust) { + case UNTRUSTED: + case TRUSTED: + trustToggle.setChecked(trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED, false); + trustToggle.setEnabled(true); + break; + case UNDECIDED: + trustToggle.setChecked(false, false); + trustToggle.setEnabled(false); + break; + } + + if (showTag) { + keyType.setText(getString(R.string.axolotl_fingerprint)); + } else { + keyType.setVisibility(View.GONE); + } + + key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); + keys.addView(view); + + } + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { final Contact contact = conversation.getContact(); diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index df20e6f2..3d65365d 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -368,7 +368,7 @@ android:layout_height="wrap_content" android:textColor="@color/black54" android:textSize="?attr/TextSizeInfo" - android:text="@string/axolotl_fingerprint"/> + android:text="@string/this_device_axolotl_fingerprint"/> + + + + + + + diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index a43f8083..64f6075c 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -4,10 +4,10 @@ android:layout_height="match_parent" > @@ -37,27 +38,28 @@ android:visibility="gone" android:textColor="@color/black54" android:textSize="?attr/TextSizeInfo"/> - + android:visibility="gone" /> - + style="@style/MaterialDesignButton"/> + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6808f1bb..f4c75ee1 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -209,6 +209,8 @@ Your fingerprint OTR fingerprint Axolotl fingerprint + Own Axolotl fingerprint + Other devices Other own Axolotl Devices Verify Decrypt diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index e8572d9d..d609b5fb 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -18,4 +18,10 @@ 16dp + + \ No newline at end of file -- cgit v1.2.3 From ec0aff4ed7982cc6db43cb6337f828f732429fd2 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 14:17:25 +0200 Subject: Encrypt files for HTTP upload in encrypted chats --- .../java/eu/siacs/conversations/http/HttpDownloadConnection.java | 6 ++++-- src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 62fe4191..a9c106ab 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -70,7 +70,8 @@ public class HttpDownloadConnection implements Transferable { String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; @@ -86,7 +87,8 @@ public class HttpDownloadConnection implements Transferable { this.file.setKey(CryptoHelper.hexToBytes(reference)); } - if (this.message.getEncryption() == Message.ENCRYPTION_OTR + if ((this.message.getEncryption() == Message.ENCRYPTION_OTR + || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index a3ab8dab..c0d4455a 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -88,7 +88,9 @@ public class HttpUploadConnection implements Transferable { this.file = mXmppConnectionService.getFileBackend().getFile(message, false); this.file.setExpectedSize(this.file.getSize()); - if (Config.ENCRYPT_ON_HTTP_UPLOADED) { + if (Config.ENCRYPT_ON_HTTP_UPLOADED + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL + || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[48]; mXmppConnectionService.getRNG().nextBytes(this.key); this.file.setKey(this.key); -- cgit v1.2.3 From 14010bf5a6198e4e53ba3f86328d061cf20b8da1 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 18:36:28 +0200 Subject: Ask for key trust when sending messages If the contact (or the own account) has keys that have UNDECIDED trust, we now drop the user into the new TrustKeysActivity, where they have to decide for each new key whether it should be TRUSTED or UNTRUSTED. --- src/main/AndroidManifest.xml | 4 + .../crypto/axolotl/AxolotlService.java | 125 +++++++---- .../conversations/persistance/DatabaseBackend.java | 9 + .../services/XmppConnectionService.java | 46 +++- .../conversations/ui/ConversationActivity.java | 56 ++++- .../conversations/ui/ConversationFragment.java | 23 +- .../siacs/conversations/ui/TrustKeysActivity.java | 237 +++++++++++++++++++++ .../eu/siacs/conversations/ui/XmppActivity.java | 9 +- .../conversations/xmpp/OnNewKeysAvailable.java | 5 + src/main/res/layout/activity_trust_keys.xml | 120 +++++++++++ src/main/res/values/strings.xml | 3 + 11 files changed, 580 insertions(+), 57 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java create mode 100644 src/main/res/layout/activity_trust_keys.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9fe37017..b0611f84 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -130,6 +130,10 @@ + getContactUndecidedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, Trust.UNDECIDED); + } + // -------------------------------------- // SessionStore // -------------------------------------- @@ -658,6 +666,14 @@ public class AxolotlService { return axolotlStore.getIdentityKeyPair().getPublicKey(); } + public Set getPendingKeys() { + return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString()); + } + + public Set getPendingKeys(Contact contact) { + return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString()); + } + private AxolotlAddress getAddressForJid(Jid jid) { return new AxolotlAddress(jid.toString(), 0); } @@ -852,14 +868,32 @@ public class AxolotlService { axolotlStore.setFingerprintTrust(fingerprint, trust); } - private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); + private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address, final boolean flushWaitingQueueAfterFetch) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId()); try { IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( Jid.fromString(address.getName()), address.getDeviceId()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket); mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + private void finish() { + AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + if (flushWaitingQueueAfterFetch) { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, + new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + processSending(message); + } + }); + } + mXmppConnectionService.newKeysAvailable(); + } + } + @Override public void onIqPacketReceived(Account account, IqPacket packet) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing..."); @@ -869,6 +903,7 @@ public class AxolotlService { if (preKeyBundleList.isEmpty() || bundle == null) { Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); return; } Random random = new Random(); @@ -876,6 +911,7 @@ public class AxolotlService { if (preKey == null) { //should never happen fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); return; } @@ -898,18 +934,7 @@ public class AxolotlService { fetchStatusMap.put(address, FetchStatus.ERROR); } - AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); - AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); - if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) - && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, - new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - processSending(message); - } - }); - } + finish(); } }); } catch (InvalidJidException e) { @@ -917,48 +942,75 @@ public class AxolotlService { } } - private boolean createSessionsIfNeeded(Conversation conversation) { - boolean newSessions = false; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Creating axolotl sessions if needed..."); + public Set findDevicesWithoutSession(final Conversation conversation) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid()); Jid contactJid = conversation.getContact().getJid().toBareJid(); Set addresses = new HashSet<>(); if(deviceIds.get(contactJid) != null) { for(Integer foreignId:this.deviceIds.get(contactJid)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+foreignId); - addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId); + if(sessions.get(address) == null) { + 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", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId); + addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + } + } } } else { Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+ownId); - addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); + AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId); + if(sessions.get(address) == null) { + 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", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); + addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); + } + } } } + + return addresses; + } + + public boolean createSessionsIfNeeded(final Conversation conversation, final boolean flushWaitingQueueAfterFetch) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set addresses = findDevicesWithoutSession(conversation); for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Processing device: " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); FetchStatus status = fetchStatusMap.get(address); - XmppAxolotlSession session = sessions.get(address); - if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { - 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..."); - session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); - sessions.put(address, session); - } else { + if ( status == null || status == FetchStatus.ERROR ) { fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(conversation, address); + this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch); newSessions = true; - } } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already fetching bundle for " + address.toString()); } } + return newSessions; } + public boolean hasPendingKeyFetches(Conversation conversation) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + ||fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); + + } + @Nullable public XmppAxolotlMessage encrypt(Message message ){ final String content; @@ -1013,10 +1065,9 @@ public class AxolotlService { }); } - public void prepareMessage(Message message) { + public void prepareMessage(final Message message) { if (!messageCache.containsKey(message.getUuid())) { - boolean newSessions = createSessionsIfNeeded(message.getConversation()); - + boolean newSessions = createSessionsIfNeeded(message.getConversation(), true); if (!newSessions) { this.processSending(message); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 39ef5d36..a2c62a8c 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -834,10 +834,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public Set loadIdentityKeys(Account account, String name) { + return loadIdentityKeys(account, name, null); + } + + public Set loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) { Set identityKeys = new HashSet<>(); Cursor cursor = getIdentityKeyCursor(account, name, false); while(cursor.moveToNext()) { + if ( trust != null && + cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)) + != trust.ordinal()) { + continue; + } try { identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); } catch (InvalidKeyException e) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 08c0b3fa..cc113cef 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -85,6 +85,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; @@ -307,6 +308,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private int rosterChangedListenerCount = 0; private OnMucRosterUpdate mOnMucRosterUpdate = null; private int mucRosterChangedListenerCount = 0; + private OnNewKeysAvailable mOnNewKeysAvailable = null; + private int newKeysAvailableListenerCount = 0; private SecureRandom mRandom; private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; @@ -1344,17 +1347,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa switchToForeground(); } this.mOnUpdateBlocklist = listener; - if (this.updateBlocklistListenerCount < 2) { - this.updateBlocklistListenerCount++; + if (this.newKeysAvailableListenerCount < 2) { + this.newKeysAvailableListenerCount++; } } } public void removeOnUpdateBlocklistListener() { synchronized (this) { - this.updateBlocklistListenerCount--; - if (this.updateBlocklistListenerCount <= 0) { - this.updateBlocklistListenerCount = 0; + this.newKeysAvailableListenerCount--; + if (this.newKeysAvailableListenerCount <= 0) { + this.newKeysAvailableListenerCount = 0; this.mOnUpdateBlocklist = null; if (checkListeners()) { switchToBackground(); @@ -1363,6 +1366,30 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void setOnNewKeysAvailableListener(final OnNewKeysAvailable listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnNewKeysAvailable = listener; + if (this.newKeysAvailableListenerCount < 2) { + this.newKeysAvailableListenerCount++; + } + } + } + + public void removeOnNewKeysAvailableListener() { + synchronized (this) { + this.newKeysAvailableListenerCount--; + if (this.newKeysAvailableListenerCount <= 0) { + this.newKeysAvailableListenerCount = 0; + this.mOnNewKeysAvailable = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1393,7 +1420,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null && this.mOnUpdateBlocklist == null - && this.mOnShowErrorToast == null); + && this.mOnShowErrorToast == null + && this.mOnNewKeysAvailable == null); } private void switchToForeground() { @@ -2281,6 +2309,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void newKeysAvailable() { + if(mOnNewKeysAvailable != null) { + mOnNewKeysAvailable.onNewKeysAvailable(); + } + } + public Account findAccountByJid(final Jid accountJid) { for (Account account : this.accounts) { if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 2e50af3b..a6cd0431 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -65,11 +65,14 @@ public class ConversationActivity extends XmppActivity public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208; + public static final int REQUEST_TRUST_KEYS_MENU = 0x0209; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; + public static final int ATTACHMENT_CHOICE_INVALID = 0x0306; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -79,6 +82,7 @@ public class ConversationActivity extends XmppActivity final private List mPendingImageUris = new ArrayList<>(); final private List mPendingFileUris = new ArrayList<>(); private Uri mPendingGeoUri = null; + private boolean forbidProcessingPendings = false; private View mContentView; @@ -401,7 +405,7 @@ public class ConversationActivity extends XmppActivity return true; } - private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { final Conversation conversation = getSelectedConversation(); final Account account = conversation.getAccount(); final OnPresenceSelected callback = new OnPresenceSelected() { @@ -537,7 +541,9 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } } else { - selectPresenceToAttachFile(attachmentChoice,encryption); + if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) { + selectPresenceToAttachFile(attachmentChoice, encryption); + } } } @@ -962,18 +968,23 @@ public class ConversationActivity extends XmppActivity this.mConversationFragment.reInit(getSelectedConversation()); } - for(Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { - attachImageToConversation(getSelectedConversation(),i.next()); - } + if(!forbidProcessingPendings) { + for (Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { + Uri foo = i.next(); + attachImageToConversation(getSelectedConversation(), foo); + } - for(Iterator i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { - attachFileToConversation(getSelectedConversation(),i.next()); - } + for (Iterator i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { + attachFileToConversation(getSelectedConversation(), i.next()); + } - if (mPendingGeoUri != null) { - attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); - mPendingGeoUri = null; + if (mPendingGeoUri != null) { + attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); + mPendingGeoUri = null; + } } + forbidProcessingPendings = false; + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); setIntent(new Intent()); } @@ -1083,6 +1094,9 @@ public class ConversationActivity extends XmppActivity attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); this.mPendingGeoUri = null; } + } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) { + this.forbidProcessingPendings = !xmppConnectionServiceBound; + mConversationFragment.onActivityResult(requestCode, resultCode, data); } } else { mPendingImageUris.clear(); @@ -1235,6 +1249,26 @@ public class ConversationActivity extends XmppActivity return getPreferences().getBoolean("indicate_received", false); } + protected boolean trustKeysIfNeeded(int requestCode) { + return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID); + } + + protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { + AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); + if(!axolotlService.getPendingKeys(mSelectedConversation.getContact()).isEmpty() + || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty()) { + axolotlService.createSessionsIfNeeded(mSelectedConversation, false); + Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class); + intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("choice", attachmentChoice); + startActivityForResult(intent, requestCode); + return true; + } else { + return false; + } + } + @Override protected void refreshUiReal() { updateConversationList(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 026c74ad..15491dea 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.ui; +import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.app.PendingIntent; @@ -11,6 +12,7 @@ import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.text.InputType; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; @@ -43,6 +45,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; 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.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -304,7 +307,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { sendPgpMessage(message); } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { - sendAxolotlMessage(message); + if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { + sendAxolotlMessage(message); + } } else { sendPlainTextMessage(message); } @@ -1128,7 +1133,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa protected void sendAxolotlMessage(final Message message) { final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; - //message.setCounterpart(conversation.getNextCounterpart()); xmppService.sendMessage(message); messageSent(); } @@ -1195,4 +1199,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } + @Override + public void onActivityResult(int requestCode, int resultCode, + final Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { + final String body = mEditMessage.getText().toString(); + Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); + sendAxolotlMessage(message); + } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { + int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); + activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption(activity.forceEncryption())); + } + } + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java new file mode 100644 index 00000000..4efa4f6c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -0,0 +1,237 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.whispersystems.libaxolotl.IdentityKey; + + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class TrustKeysActivity extends XmppActivity implements OnNewKeysAvailable { + private Jid accountJid; + private Jid contactJid; + + private Contact contact; + private TextView ownKeysTitle; + private LinearLayout ownKeys; + private LinearLayout ownKeysCard; + private TextView foreignKeysTitle; + private LinearLayout foreignKeys; + private LinearLayout foreignKeysCard; + private Button mSaveButton; + private Button mCancelButton; + + private final Map ownKeysToTrust = new HashMap<>(); + private final Map foreignKeysToTrust = new HashMap<>(); + + private final OnClickListener mSaveButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + commitTrusts(); + Intent data = new Intent(); + data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID)); + setResult(RESULT_OK, data); + finish(); + } + }; + + private final OnClickListener mCancelButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }; + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + populateView(); + } + + @Override + protected String getShareableUri() { + if (contact != null) { + return contact.getShareableUri(); + } else { + return ""; + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_trust_keys); + try { + this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); + } catch (final InvalidJidException ignored) { + } + try { + this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); + } catch (final InvalidJidException ignored) { + } + + ownKeysTitle = (TextView) findViewById(R.id.own_keys_title); + ownKeys = (LinearLayout) findViewById(R.id.own_keys_details); + ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card); + foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title); + foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details); + foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card); + mCancelButton = (Button) findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(mCancelButtonListener); + mSaveButton = (Button) findViewById(R.id.save_button); + mSaveButton.setOnClickListener(mSaveButtonListener); + + + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + private void populateView() { + setTitle(getString(R.string.trust_keys)); + ownKeys.removeAllViews(); + foreignKeys.removeAllViews(); + boolean hasOwnKeys = false; + boolean hasForeignKeys = false; + for(final IdentityKey identityKey : ownKeysToTrust.keySet()) { + hasOwnKeys = true; + addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, + Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ownKeysToTrust.put(identityKey, isChecked); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + null + ); + } + for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) { + hasForeignKeys = true; + addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, + Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + foreignKeysToTrust.put(identityKey, isChecked); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + null + ); + } + + if(hasOwnKeys) { + ownKeysTitle.setText(accountJid.toString()); + ownKeysCard.setVisibility(View.VISIBLE); + } + if(hasForeignKeys) { + foreignKeysTitle.setText(contactJid.toString()); + foreignKeysCard.setVisibility(View.VISIBLE); + } + } + + private void getFingerprints(final Account account) { + Set ownKeysSet = account.getAxolotlService().getPendingKeys(); + for(final IdentityKey identityKey : ownKeysSet) { + if(!ownKeysToTrust.containsKey(identityKey)) { + ownKeysToTrust.put(identityKey, false); + } + } + Set foreignKeysSet = account.getAxolotlService().getPendingKeys(contact); + for(final IdentityKey identityKey : foreignKeysSet) { + if(!foreignKeysToTrust.containsKey(identityKey)) { + foreignKeysToTrust.put(identityKey, false); + } + } + } + + @Override + public void onBackendConnected() { + if ((accountJid != null) && (contactJid != null)) { + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + ownKeysToTrust.clear(); + foreignKeysToTrust.clear(); + getFingerprints(account); + + Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false); + if(account.getAxolotlService().hasPendingKeyFetches(conversation)) { + lock(); + } + + populateView(); + } + } + + @Override + public void onNewKeysAvailable() { + runOnUiThread(new Runnable() { + @Override + public void run() { + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + unlock(); + getFingerprints(account); + refreshUi(); + } + }); + } + + private void commitTrusts() { + for(IdentityKey identityKey:ownKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + Trust.fromBoolean(ownKeysToTrust.get(identityKey))); + } + for(IdentityKey identityKey:foreignKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); + } + } + + private void unlock() { + mSaveButton.setEnabled(true); + mSaveButton.setText(getString(R.string.done)); + mSaveButton.setTextColor(getPrimaryTextColor()); + } + + private void lock() { + mSaveButton.setEnabled(false); + mSaveButton.setText(getString(R.string.fetching_keys)); + mSaveButton.setTextColor(getSecondaryTextColor()); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 9dfece2f..00322452 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -84,6 +84,7 @@ import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinde import eu.siacs.conversations.ui.widget.Switch; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -296,6 +297,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); } + if (this instanceof OnNewKeysAvailable) { + this.xmppConnectionService.setOnNewKeysAvailableListener((OnNewKeysAvailable) this); + } } protected void unregisterListeners() { @@ -317,6 +321,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.removeOnShowErrorToastListener(); } + if (this instanceof OnNewKeysAvailable) { + this.xmppConnectionService.removeOnNewKeysAvailableListener(); + } } @Override @@ -452,7 +459,7 @@ public abstract class XmppActivity extends Activity { @Override public void userInputRequried(PendingIntent pi, - Account account) { + Account account) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java b/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java new file mode 100644 index 00000000..59dc1c1e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public interface OnNewKeysAvailable { + public void onNewKeysAvailable(); +} diff --git a/src/main/res/layout/activity_trust_keys.xml b/src/main/res/layout/activity_trust_keys.xml new file mode 100644 index 00000000..c535d51d --- /dev/null +++ b/src/main/res/layout/activity_trust_keys.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +