Merge branch 'CryptoNextBeta' into development

Andreas Straub created

* CryptoNextBeta: (60 commits)
  Lock TrustKeys if no trusted keys are available
  Optimize imports
  Use MD style for key trust toggle switch
  Fix set/remove OnUpdateBlocklistListener
  Fix axolotl database migration
  Remove device list from EditAccount
  Add clear devices to overflow menu in EditAccount
  Ask for key trust when sending messages
  Encrypt files for HTTP upload in encrypted chats
  Refactor trust key ui and show in account details
  Send correct body for HTTP files
  Handle file transmission properly in axolotl
  Remove unneccessary code
  Fix trust status for outgoing messages
  Don't merge messages with different trust statuses
  Fix copying of axolotl keys to clipboard
  Add refresh icon to v21 theme
  Disable Axolotl option if not usable
  Show trust status of messages' originating session
  Add key trust toggle to ContactDetailsActivity
  ...

Change summary

build.gradle                                                                        |    2 
src/main/AndroidManifest.xml                                                        |    4 
src/main/java/eu/siacs/conversations/crypto/OtrService.java                         |   31 
src/main/java/eu/siacs/conversations/crypto/PgpEngine.java                          |   15 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java             | 1136 
src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java |    4 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java         |  194 
src/main/java/eu/siacs/conversations/entities/Account.java                          |   11 
src/main/java/eu/siacs/conversations/entities/Contact.java                          |  141 
src/main/java/eu/siacs/conversations/entities/Conversation.java                     |    6 
src/main/java/eu/siacs/conversations/entities/DownloadableFile.java                 |    5 
src/main/java/eu/siacs/conversations/entities/Message.java                          |   28 
src/main/java/eu/siacs/conversations/entities/MucOptions.java                       |    4 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java               |    4 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                     |   60 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                |   26 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java               |    8 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java                 |    6 
src/main/java/eu/siacs/conversations/parser/AbstractParser.java                     |    1 
src/main/java/eu/siacs/conversations/parser/IqParser.java                           |  163 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                      |   41 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java               |  510 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java                   |   30 
src/main/java/eu/siacs/conversations/services/EventReceiver.java                    |    3 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java            |   73 
src/main/java/eu/siacs/conversations/ui/AboutPreference.java                        |    1 
src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java                 |    1 
src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java                  |    6 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java              |    3 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                 |   12 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java                   |   76 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                   |   34 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java                    |  115 
src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java                  |   17 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                       |   24 
src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java                      |    1 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java                      |  255 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                           |   87 
src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java                 |   16 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java            |    2 
src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java              |    8 
src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java                |   20 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java                 |   15 
src/main/java/eu/siacs/conversations/ui/widget/Switch.java                          |    2 
src/main/java/eu/siacs/conversations/utils/CryptoHelper.java                        |    9 
src/main/java/eu/siacs/conversations/utils/DNSHelper.java                           |   28 
src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java                    |    4 
src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java                     |   24 
src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java       |    4 
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                         |    7 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                            |   12 
src/main/java/eu/siacs/conversations/xml/Element.java                               |    5 
src/main/java/eu/siacs/conversations/xml/XmlReader.java                             |   16 
src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java                   |    5 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                       |    1 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java              |   13 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java       |    8 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java         |    6 
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                           |    4 
src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java                |    7 
src/main/res/drawable-hdpi/ic_action_done.png                                       |    0 
src/main/res/drawable-hdpi/ic_done_black_24dp.png                                   |    0 
src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png                              |    0 
src/main/res/drawable-mdpi/ic_action_done.png                                       |    0 
src/main/res/drawable-mdpi/ic_done_black_24dp.png                                   |    0 
src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png                              |    0 
src/main/res/drawable-xhdpi/ic_action_done.png                                      |    0 
src/main/res/drawable-xhdpi/ic_done_black_24dp.png                                  |    0 
src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png                             |    0 
src/main/res/drawable-xxhdpi/ic_done_black_24dp.png                                 |    0 
src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png                            |    0 
src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png                                |    0 
src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png                           |    0 
src/main/res/layout/activity_edit_account.xml                                       |   89 
src/main/res/layout/activity_trust_keys.xml                                         |  120 
src/main/res/layout/contact_key.xml                                                 |   34 
src/main/res/menu/editaccount.xml                                                   |    4 
src/main/res/menu/encryption_choices.xml                                            |    3 
src/main/res/values-v21/themes.xml                                                  |    2 
src/main/res/values/attrs.xml                                                       |    1 
src/main/res/values/strings.xml                                                     |   15 
src/main/res/values/themes.xml                                                      |    1 
82 files changed, 3,315 insertions(+), 308 deletions(-)

Detailed changes

build.gradle πŸ”—

@@ -37,6 +37,8 @@ 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'
+	compile 'com.kyleduo.switchbutton:library:1.2.8'
 }
 
 android {

src/main/AndroidManifest.xml πŸ”—

@@ -130,6 +130,10 @@
                 <data android:mimeType="image/*" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".ui.TrustKeysActivity"
+            android:label="@string/trust_keys"
+            android:windowSoftInputMode="stateAlwaysHidden"/>
         <activity
             android:name="de.duenndns.ssl.MemorizingActivity"
             android:theme="@style/ConversationsTheme"

src/main/java/eu/siacs/conversations/crypto/OtrService.java πŸ”—

@@ -1,5 +1,20 @@
 package eu.siacs.conversations.crypto;
 
+import android.util.Log;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.FragmenterInstructions;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import java.math.BigInteger;
 import java.security.KeyFactory;
 import java.security.KeyPair;
@@ -11,31 +26,15 @@ import java.security.spec.DSAPrivateKeySpec;
 import java.security.spec.DSAPublicKeySpec;
 import java.security.spec.InvalidKeySpecException;
 
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.util.Log;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xmpp.chatstate.ChatState;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
-import net.java.otr4j.OtrEngineHost;
-import net.java.otr4j.OtrException;
-import net.java.otr4j.OtrPolicy;
-import net.java.otr4j.OtrPolicyImpl;
-import net.java.otr4j.crypto.OtrCryptoEngineImpl;
-import net.java.otr4j.crypto.OtrCryptoException;
-import net.java.otr4j.session.InstanceTag;
-import net.java.otr4j.session.SessionID;
-import net.java.otr4j.session.FragmenterInstructions;
-
 public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
 
 	private Account account;

src/main/java/eu/siacs/conversations/crypto/PgpEngine.java πŸ”—

@@ -1,5 +1,13 @@
 package eu.siacs.conversations.crypto;
 
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
@@ -9,10 +17,6 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 
-import org.openintents.openpgp.OpenPgpSignatureResult;
-import org.openintents.openpgp.util.OpenPgpApi;
-import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
-
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -22,9 +26,6 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.UiCallback;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.net.Uri;
 
 public class PgpEngine {
 	private OpenPgpApi api;

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java πŸ”—

@@ -0,0 +1,1136 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+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;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.util.Arrays;
+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.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 {
+
+	public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
+	public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
+	public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
+
+	public static final String LOGPREFIX = "AxolotlService";
+
+	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 Map<Jid, Set<Integer>> deviceIds;
+	private final Map<String, MessagePacket> messageCache;
+	private final FetchStatusMap fetchStatusMap;
+	private final SerialSingleThreadExecutor executor;
+
+	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 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 FINGERPRINT = "fingerprint";
+		public static final String NAME = "name";
+		public static final String TRUSTED = "trusted";
+		public static final String OWN = "ownkey";
+
+		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 IdentityKeyPair identityKeyPair;
+		private 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";
+				}
+			}
+
+			public static Trust fromBoolean(Boolean trusted) {
+				return trusted?TRUSTED:UNTRUSTED;
+			}
+		};
+
+		private static IdentityKeyPair generateIdentityKeyPair() {
+			Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair...");
+			ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
+			IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
+					identityKeyPairKeys.getPrivateKey());
+			return ownKey;
+		}
+
+		private static int generateRegistrationId() {
+			Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID...");
+			int reg_id = KeyHelper.generateRegistrationId(true);
+			return reg_id;
+		}
+
+		public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
+			this.account = account;
+			this.mXmppConnectionService = service;
+			this.localRegistrationId = loadRegistrationId();
+			this.currentPreKeyId = loadCurrentPreKeyId();
+			for (SignedPreKeyRecord record : loadSignedPreKeys()) {
+				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId());
+			}
+		}
+
+		public int getCurrentPreKeyId() {
+			return currentPreKeyId;
+		}
+
+		// --------------------------------------
+		// IdentityKeyStore
+		// --------------------------------------
+
+		private IdentityKeyPair loadIdentityKeyPair() {
+			String ownName = account.getJid().toBareJid().toString();
+			IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
+					ownName);
+
+			if (ownKey != null) {
+				return ownKey;
+			} else {
+				Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName);
+				ownKey = generateIdentityKeyPair();
+				mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
+			}
+			return ownKey;
+		}
+
+		private int loadRegistrationId() {
+			return loadRegistrationId(false);
+		}
+
+		private int loadRegistrationId(boolean regenerate) {
+			String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
+			int reg_id;
+			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());
+				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, AxolotlService.getLogprefix(account)+"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.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid());
+				reg_id = 0;
+			}
+			return reg_id;
+		}
+
+		public void regenerate() {
+			mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
+			account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
+			identityKeyPair = loadIdentityKeyPair();
+			localRegistrationId = loadRegistrationId(true);
+			currentPreKeyId = 0;
+			mXmppConnectionService.updateAccountUi();
+		}
+
+		/**
+		 * Get the local client's identity key pair.
+		 *
+		 * @return The local client's persistent identity key pair.
+		 */
+		@Override
+		public IdentityKeyPair getIdentityKeyPair() {
+			if(identityKeyPair == null) {
+				identityKeyPair = loadIdentityKeyPair();
+			}
+			return identityKeyPair;
+		}
+
+		/**
+		 * Return the local client's registration ID.
+		 * <p/>
+		 * 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
+		 * <p/>
+		 * 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) {
+			if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
+				mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
+			}
+		}
+
+		/**
+		 * Verify a remote client's identity key.
+		 * <p/>
+		 * 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) {
+			return true;
+		}
+
+		public Trust getFingerprintTrust(String fingerprint) {
+			return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
+		}
+
+		public void setFingerprintTrust(String fingerprint, Trust trust) {
+			mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
+		}
+
+		public Set<IdentityKey> getContactUndecidedKeys(String bareJid) {
+			return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, Trust.UNDECIDED);
+		}
+
+		public long getContactNumTrustedKeys(String bareJid) {
+			return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
+		}
+
+		// --------------------------------------
+		// SessionStore
+		// --------------------------------------
+
+		/**
+		 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
+		 * or a new SessionRecord if one does not currently exist.
+		 * <p/>
+		 * 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<Integer> 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: " + 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, AxolotlService.getLogprefix(account)+"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<SignedPreKeyRecord> 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 final SessionCipher cipher;
+		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;
+		}
+
+		public Integer getPreKeyId() {
+			return preKeyId;
+		}
+
+		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());
+					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");
+					WhisperMessage message = new WhisperMessage(incomingHeader.getContents());
+					plaintext = cipher.decrypt(message);
+				} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
+					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"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;
+		}
+
+		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<T> {
+		protected Map<String, Map<Integer, T>> 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<Integer, T> 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<Integer, T> devices = map.get(address.getName());
+				if (devices == null) {
+					return null;
+				}
+				return devices.get(address.getDeviceId());
+			}
+		}
+
+		public Map<Integer, T> getAll(AxolotlAddress address) {
+			synchronized (MAP_LOCK) {
+				Map<Integer, T> devices = map.get(address.getName());
+				if (devices == null) {
+					return new HashMap<>();
+				}
+				return devices;
+			}
+		}
+
+		public boolean hasAny(AxolotlAddress address) {
+			synchronized (MAP_LOCK) {
+				Map<Integer, T> devices = map.get(address.getName());
+				return devices != null && !devices.isEmpty();
+			}
+		}
+
+		public void clear() {
+			map.clear();
+		}
+
+	}
+
+	private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
+		private final XmppConnectionService xmppConnectionService;
+		private final Account account;
+
+		public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
+			super();
+			this.xmppConnectionService = service;
+			this.account = account;
+			this.fillMap(store);
+		}
+
+		private void fillMap(SQLiteAxolotlStore store) {
+			for (Contact contact : account.getRoster().getContacts()) {
+				Jid bareJid = contact.getJid().toBareJid();
+				if (bareJid == null) {
+					continue; // FIXME: handle this?
+				}
+				String address = bareJid.toString();
+				List<Integer> deviceIDs = store.getSubDeviceSessions(address);
+				for (Integer deviceId : deviceIDs) {
+					AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId);
+					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building session for remote address: "+axolotlAddress.toString());
+					String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
+					this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
+				}
+			}
+		}
+
+		@Override
+		public void put(AxolotlAddress address, XmppAxolotlSession value) {
+			super.put(address, value);
+			xmppConnectionService.syncRosterToDisk(account);
+		}
+	}
+
+	private static enum FetchStatus {
+		PENDING,
+		SUCCESS,
+		ERROR
+	}
+
+	private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
+
+	}
+	
+	public static String getLogprefix(Account account) {
+		return LOGPREFIX+" ("+account.getJid().toBareJid().toString()+"): ";
+	}
+
+	public AxolotlService(Account account, XmppConnectionService connectionService) {
+		this.mXmppConnectionService = connectionService;
+		this.account = account;
+		this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
+		this.deviceIds = new HashMap<>();
+		this.messageCache = new HashMap<>();
+		this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
+		this.fetchStatusMap = new FetchStatusMap();
+		this.executor = new SerialSingleThreadExecutor();
+	}
+
+	public IdentityKey getOwnPublicKey() {
+		return axolotlStore.getIdentityKeyPair().getPublicKey();
+	}
+
+	public Set<IdentityKey> getPendingKeys() {
+		return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString());
+	}
+
+	public Set<IdentityKey> getPendingKeys(Contact contact) {
+		return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString());
+	}
+
+	public long getNumTrustedKeys(Contact contact) {
+		return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
+	}
+
+	private AxolotlAddress getAddressForJid(Jid jid) {
+		return new AxolotlAddress(jid.toString(), 0);
+	}
+
+	private Set<XmppAxolotlSession> findOwnSessions() {
+		AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
+		Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
+		return ownDeviceSessions;
+	}
+
+	private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
+		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
+		Set<XmppAxolotlSession> 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 void regenerateKeys() {
+		axolotlStore.regenerate();
+		sessions.clear();
+		fetchStatusMap.clear();
+		publishBundlesIfNeeded();
+		publishOwnDeviceIdIfNeeded();
+	}
+
+	public int getOwnDeviceId() {
+		return axolotlStore.loadRegistrationId();
+	}
+
+	public Set<Integer> getOwnDeviceIds() {
+		return this.deviceIds.get(account.getJid().toBareJid());
+	}
+
+	public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
+		if(deviceIds.contains(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, AxolotlService.getLogprefix(account)+"Adding Device ID:"+ jid + ":"+i);
+		}
+		this.deviceIds.put(jid, deviceIds);
+		publishOwnDeviceIdIfNeeded();
+	}
+
+	public void wipeOtherPepDevices() {
+		Set<Integer> deviceIds = new HashSet<>();
+		deviceIds.add(getOwnDeviceId());
+		IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Wiping all other devices from Pep:" + publish);
+		mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				// TODO: implement this!
+			}
+		});
+	}
+
+	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);
+				Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+				if (deviceIds == null) {
+					deviceIds = new HashSet<Integer>();
+				}
+				if (!deviceIds.contains(getOwnDeviceId())) {
+					deviceIds.add(getOwnDeviceId());
+					IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+					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) {
+							// TODO: implement this!
+						}
+					});
+				}
+			}
+		});
+	}
+
+	public void publishBundlesIfNeeded() {
+		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
+		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
+				Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
+				boolean flush = false;
+				if (bundle == null) {
+					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.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.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
+						changed = true;
+					}
+
+					// Validate signedPreKeyRecord + ID
+					SignedPreKeyRecord signedPreKeyRecord;
+					int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
+					try {
+						signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+						if ( flush
+								||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+								|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
+							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.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;
+					}
+
+					// Validate PreKeys
+					Set<PreKeyRecord> 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<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
+								axolotlStore.getCurrentPreKeyId()+1, newKeys);
+						preKeyRecords.addAll(newRecords);
+						for (PreKeyRecord record : newRecords) {
+							axolotlStore.storePreKey(record.getId(), record);
+						}
+						changed = true;
+						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding " + newKeys + " new preKeys to PEP.");
+					}
+
+
+					if(changed) {
+						IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
+								signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
+								preKeyRecords, getOwnDeviceId());
+						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
+						mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+							@Override
+							public void onIqPacketReceived(Account account, IqPacket packet) {
+								// TODO: implement this!
+								Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Published bundle, got: " + packet);
+							}
+						});
+					}
+				} catch (InvalidKeyException e) {
+						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
+						return;
+				}
+			}
+		});
+	}
+
+	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 fingerprint) {
+		return axolotlStore.getFingerprintTrust(fingerprint);
+	}
+
+	public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) {
+		axolotlStore.setFingerprintTrust(fingerprint, trust);
+	}
+
+	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...");
+					final IqParser parser = mXmppConnectionService.getIqParser();
+					final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
+					final PreKeyBundle bundle = parser.bundle(packet);
+					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();
+					final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
+					if (preKey == null) {
+						//should never happen
+						fetchStatusMap.put(address, FetchStatus.ERROR);
+						finish();
+						return;
+					}
+
+					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());
+
+					try {
+						SessionBuilder builder = new SessionBuilder(axolotlStore, address);
+						builder.process(preKeyBundle);
+						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) {
+						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error building session for " + address + ": "
+								+ e.getClass().getName() + ", " + e.getMessage());
+						fetchStatusMap.put(address, FetchStatus.ERROR);
+					}
+
+					finish();
+				}
+			});
+		} catch (InvalidJidException e) {
+			Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got address with invalid jid: " + address.getName());
+		}
+	}
+
+	public Set<AxolotlAddress> 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<AxolotlAddress> addresses = new HashSet<>();
+		if(deviceIds.get(contactJid) != null) {
+			for(Integer foreignId:this.deviceIds.get(contactJid)) {
+				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!");
+		}
+		if(deviceIds.get(account.getJid().toBareJid()) != null) {
+			for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) {
+				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<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
+		for (AxolotlAddress address : addresses) {
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
+			FetchStatus status = fetchStatusMap.get(address);
+			if ( status == null || status == FetchStatus.ERROR ) {
+					fetchStatusMap.put(address, FetchStatus.PENDING);
+					this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch);
+					newSessions = true;
+			} else {
+				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;
+		if (message.hasFileOnRemoteHost()) {
+				content = message.getFileParams().url.toString();
+			} else {
+				content = message.getBody();
+			}
+		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
+				getOwnDeviceId(), content);
+
+		if(findSessionsforContact(message.getContact()).isEmpty()) {
+			return null;
+		}
+		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers...");
+		for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
+			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, AxolotlService.getLogprefix(account)+"Building axolotl own headers...");
+		for (XmppAxolotlSession session : findOwnSessions()) {
+			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString());
+			//        if(!session.isTrusted()) {
+			// TODO: handle this properly
+			//          continue;
+			//    }
+			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
+		}
+
+		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);
+					//mXmppConnectionService.updateConversationUi();
+				} else {
+					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Generated message, caching: " + message.getUuid());
+					messageCache.put(message.getUuid(), packet);
+					mXmppConnectionService.resendMessage(message);
+				}
+			}
+		});
+	}
+
+	public void prepareMessage(final Message message) {
+		if (!messageCache.containsKey(message.getUuid())) {
+			boolean newSessions = createSessionsIfNeeded(message.getConversation(), true);
+			if (!newSessions) {
+				this.processSending(message);
+			}
+		}
+	}
+
+	public MessagePacket fetchPacketFromCache(Message message) {
+		MessagePacket packet = messageCache.get(message.getUuid());
+		if (packet != null) {
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache hit: " + message.getUuid());
+			messageCache.remove(message.getUuid());
+		} else {
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache miss: " + message.getUuid());
+		}
+		return packet;
+	}
+
+	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) {
+		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
+		AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
+				message.getSenderDeviceId());
+
+		boolean newSession = false;
+		XmppAxolotlSession session = sessions.get(senderAddress);
+		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
+			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;
+		}
+
+		for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
+			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) {
+					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message...");
+					plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint());
+				}
+				Integer preKeyId = session.getPreKeyId();
+				if (preKeyId != null) {
+					publishBundlesIfNeeded();
+					session.resetPreKeyId();
+				}
+				break;
+			}
+		}
+
+		if (newSession && plaintextMessage != null) {
+			sessions.put(senderAddress, session);
+		}
+
+		return plaintextMessage;
+	}
+}

src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java πŸ”—

@@ -0,0 +1,194 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.util.Base64;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+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.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class XmppAxolotlMessage {
+	private byte[] innerKey;
+	private byte[] ciphertext;
+	private byte[] iv;
+	private final Set<XmppAxolotlMessageHeader> headers;
+	private final Jid from;
+	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 <header> 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;
+		private final String fingerprint;
+
+		public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) {
+			this.session = session;
+			this.plaintext = plaintext;
+			this.fingerprint = fingerprint;
+		}
+
+		public String getPlaintext() {
+			return plaintext;
+		}
+
+		public AxolotlService.XmppAxolotlSession getSession() {
+			return session;
+		}
+
+		public String getFingerprint() {
+			return fingerprint;
+		}
+	}
+
+	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()) {
+			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(Jid from, int sourceDeviceId, String plaintext) {
+		this.from = from;
+		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 Jid getFrom() {
+		return this.from;
+	}
+
+	public int getSenderDeviceId() {
+		return sourceDeviceId;
+	}
+
+	public byte[] getCiphertext() {
+		return ciphertext;
+	}
+
+	public Set<XmppAxolotlMessageHeader> 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, String fingerprint) {
+		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, fingerprint);
+
+		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+				| InvalidAlgorithmParameterException | IllegalBlockSizeException
+				| BadPaddingException e) {
+			throw new AssertionError(e);
+		}
+		return plaintextMessage;
+	}
+}

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;
@@ -254,6 +256,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);
@@ -277,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() {

src/main/java/eu/siacs/conversations/entities/Contact.java πŸ”—

@@ -183,20 +183,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,60 +283,65 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	public ArrayList<String> getOtrFingerprints() {
-		final ArrayList<String> fingerprints = new ArrayList<String>();
-		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<String> fingerprints = new ArrayList<String>();
+			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;
-		}
-		try {
-			JSONArray fingerprints;
-			if (!this.keys.has("otr_fingerprints")) {
-				fingerprints = new JSONArray();
-
-			} else {
-				fingerprints = this.keys.getJSONArray("otr_fingerprints");
+		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;
 			}
-			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")) {
-			try {
-				return this.keys.getLong("pgp_keyid");
-			} catch (JSONException e) {
+		synchronized (this.keys) {
+			if (this.keys.has("pgp_keyid")) {
+				try {
+					return this.keys.getLong("pgp_keyid");
+				} catch (JSONException e) {
+					return 0;
+				}
+			} else {
 				return 0;
 			}
-		} else {
-			return 0;
 		}
 	}
 
 	public void setPgpKeyId(long keyId) {
-		try {
-			this.keys.put("pgp_keyid", keyId);
-		} catch (final JSONException ignored) {
-
+		synchronized (this.keys) {
+			try {
+				this.keys.put("pgp_keyid", keyId);
+			} catch (final JSONException ignored) {
+			}
 		}
 	}
 
@@ -441,24 +448,26 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	public boolean deleteOtrFingerprint(String fingerprint) {
-		boolean success = false;
-		try {
-			if (this.keys.has("otr_fingerprints")) {
-				JSONArray newPrints = new JSONArray();
-				JSONArray oldPrints = this.keys
-					.getJSONArray("otr_fingerprints");
-				for (int i = 0; i < oldPrints.length(); ++i) {
-					if (!oldPrints.getString(i).equals(fingerprint)) {
-						newPrints.put(oldPrints.getString(i));
-					} else {
-						success = true;
+		synchronized (this.keys) {
+			boolean success = false;
+			try {
+				if (this.keys.has("otr_fingerprints")) {
+					JSONArray newPrints = new JSONArray();
+					JSONArray oldPrints = this.keys
+							.getJSONArray("otr_fingerprints");
+					for (int i = 0; i < oldPrints.length(); ++i) {
+						if (!oldPrints.getString(i).equals(fingerprint)) {
+							newPrints.put(oldPrints.getString(i));
+						} else {
+							success = true;
+						}
 					}
+					this.keys.put("otr_fingerprints", newPrints);
 				}
-				this.keys.put("otr_fingerprints", newPrints);
+				return success;
+			} catch (JSONException e) {
+				return false;
 			}
-			return success;
-		} catch (JSONException e) {
-			return false;
 		}
 	}
 

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);
-						}
+				}
 			}
 		}
 	}

src/main/java/eu/siacs/conversations/entities/DownloadableFile.java πŸ”—

@@ -1,12 +1,13 @@
 package eu.siacs.conversations.entities;
 
+import android.util.Log;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.URLConnection;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.Key;
@@ -22,8 +23,6 @@ import javax.crypto.spec.SecretKeySpec;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.MimeUtils;
 
-import android.util.Log;
-
 public class DownloadableFile extends File {
 
 	private static final long serialVersionUID = 2247012619505115863L;

src/main/java/eu/siacs/conversations/entities/Message.java πŸ”—

@@ -8,6 +8,7 @@ import java.net.URL;
 import java.util.Arrays;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.utils.UIHelper;
@@ -34,6 +35,7 @@ public class Message extends AbstractEntity {
 	public static final int ENCRYPTION_OTR = 2;
 	public static final int ENCRYPTION_DECRYPTED = 3;
 	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+	public static final int ENCRYPTION_AXOLOTL = 5;
 
 	public static final int TYPE_TEXT = 0;
 	public static final int TYPE_IMAGE = 1;
@@ -52,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 ";
 
 
@@ -73,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() {
 
@@ -94,6 +98,7 @@ public class Message extends AbstractEntity {
 				TYPE_TEXT,
 				null,
 				null,
+				null,
 				null);
 		this.conversation = conversation;
 	}
@@ -101,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;
@@ -114,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) {
@@ -150,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) {
@@ -184,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;
 	}
 
@@ -391,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()
 				);
 	}
 
@@ -663,4 +672,17 @@ public class Message extends AbstractEntity {
 		public int width = 0;
 		public int height = 0;
 	}
+
+	public void setAxolotlFingerprint(String fingerprint) {
+		this.axolotlFingerprint = fingerprint;
+	}
+
+	public String getAxolotlFingerprint() {
+		return axolotlFingerprint;
+	}
+
+	public boolean isTrusted() {
+		return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
+				== AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED;
+	}
 }

src/main/java/eu/siacs/conversations/entities/MucOptions.java πŸ”—

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.entities;
 
+import android.annotation.SuppressLint;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -11,8 +13,6 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 
-import android.annotation.SuppressLint;
-
 @SuppressLint("DefaultLocale")
 public class MucOptions {
 

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"

src/main/java/eu/siacs/conversations/generator/IqGenerator.java πŸ”—

@@ -1,15 +1,23 @@
 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 java.util.Set;
 
+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;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.Xmlns;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.forms.Data;
@@ -115,6 +123,56 @@ 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 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 publishDeviceIds(final Set<Integer> 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 publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
+	                               final Set<PreKeyRecord> 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");
+		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));
+
+		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_BUNDLES+":"+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");

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java πŸ”—

@@ -1,12 +1,18 @@
 package eu.siacs.conversations.generator;
 
+import android.util.Log;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
 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.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
@@ -59,6 +65,22 @@ public class MessageGenerator extends AbstractGenerator {
 		delay.setAttribute("stamp", mDateFormat.format(date));
 	}
 
+	public MessagePacket generateAxolotlChat(Message message) {
+		return generateAxolotlChat(message, false);
+	}
+
+	public MessagePacket generateAxolotlChat(Message message, boolean addDelay) {
+		MessagePacket packet = preparePacket(message, addDelay);
+		AxolotlService service = message.getConversation().getAccount().getAxolotlService();
+		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;
+		}
+		packet.setAxolotlMessage(axolotlMessage.toXml());
+		return packet;
+	}
+
 	public MessagePacket generateOtrChat(Message message) {
 		return generateOtrChat(message, false);
 	}

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java πŸ”—

@@ -18,9 +18,9 @@ import javax.net.ssl.SSLHandshakeException;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 
@@ -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);
 					}

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java πŸ”—

@@ -14,9 +14,9 @@ import javax.net.ssl.HttpsURLConnection;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.UiCallback;
@@ -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);

src/main/java/eu/siacs/conversations/parser/AbstractParser.java πŸ”—

@@ -11,7 +11,6 @@ import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.jid.Jid;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public abstract class AbstractParser {
 

src/main/java/eu/siacs/conversations/parser/IqParser.java πŸ”—

@@ -1,11 +1,25 @@
 package eu.siacs.conversations.parser;
 
+import android.support.annotation.NonNull;
+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.HashSet;
+import java.util.List;
+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;
@@ -71,6 +85,155 @@ 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");
+	}
+
+	@NonNull
+	public Set<Integer> deviceIds(final Element item) {
+		Set<Integer> deviceIds = new HashSet<>();
+		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, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> 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, AxolotlService.LOGPREFIX+" : "+"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,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
+		}
+		return identityKey;
+	}
+
+	public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
+		Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
+		Element item = getItem(packet);
+		if (item == null) {
+			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
+			return null;
+		}
+		final Element bundleElement = item.findChild("bundle");
+		if(bundleElement == null) {
+			return null;
+		}
+		final Element prekeysElement = bundleElement.findChild("prekeys");
+		if(prekeysElement == null) {
+			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
+			return null;
+		}
+		for(Element preKeyPublicElement : prekeysElement.getChildren()) {
+			if(!preKeyPublicElement.getName().equals("preKeyPublic")){
+				Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"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, AxolotlService.LOGPREFIX+" : "+"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<PreKeyBundle> preKeys(final IqPacket preKeys) {
+		List<PreKeyBundle> bundles = new ArrayList<>();
+		Map<Integer, ECPublicKey> 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)) {

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.Set;
+
 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,20 @@ public class MessageParser extends AbstractParser implements
 		}
 	}
 
+	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(from.toBareJid(), axolotlMessage);
+		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage);
+		if(plaintextMessage != null) {
+			finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
+			finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
+		}
+
+		return finishedMessage;
+	}
+
 	private class Invite {
 		Jid jid;
 		String password;
@@ -170,6 +188,13 @@ public class MessageParser extends AbstractParser implements
 				mXmppConnectionService.updateConversationUi();
 				mXmppConnectionService.updateAccountUi();
 			}
+		} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
+			Element item = items.findChild("item");
+			Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+			AxolotlService axolotlService = account.getAxolotlService();
+			axolotlService.registerDevices(from, deviceIds);
+			mXmppConnectionService.updateAccountUi();
 		}
 	}
 
@@ -232,8 +257,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 +287,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,8 +320,13 @@ 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 if (pgpEncrypted != null) {
+				message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
+			} else if (axolotlEncrypted != null) {
+				message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
+				if (message == null) {
+					return;
+				}
 			} else {
 				message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
 			}

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java πŸ”—

@@ -1,10 +1,32 @@
 package eu.siacs.conversations.persistance;
 
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+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;
+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;
@@ -13,19 +35,12 @@ import eu.siacs.conversations.entities.Roster;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteCantOpenDatabaseException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
 public class DatabaseBackend extends SQLiteOpenHelper {
 
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 14;
+	private static final int DATABASE_VERSION = 15;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -33,12 +48,66 @@ 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 + ", "
 			+ Contact.JID + ") ON CONFLICT REPLACE);";
 
+	private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+			+ AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+				+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
+				+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+				+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+					+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+				+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+				+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+					+ AxolotlService.SQLiteAxolotlStore.ID
+				+ ") ON CONFLICT REPLACE"
+			+");";
+
+	private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+			+ AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+				+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
+				+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+				+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+					+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+				+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+				+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+					+ AxolotlService.SQLiteAxolotlStore.ID
+				+ ") ON CONFLICT REPLACE"+
+			");";
+
+	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.KEY + " TEXT, FOREIGN KEY("
+					+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+				+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+				+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+					+ AxolotlService.SQLiteAxolotlStore.NAME + ", "
+					+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID
+				+ ") 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.FINGERPRINT + " TEXT, "
+			+ AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+			+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+			+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+			+ ") 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) {
 		super(context, DATABASE_NAME, null, DATABASE_VERSION);
 	}
@@ -69,12 +138,17 @@ 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
 				+ ") ON DELETE CASCADE);");
 
 		db.execSQL(CREATE_CONTATCS_STATEMENT);
+		db.execSQL(CREATE_SESSIONS_STATEMENT);
+		db.execSQL(CREATE_PREKEYS_STATEMENT);
+		db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
+		db.execSQL(CREATE_IDENTITIES_STATEMENT);
 	}
 
 	@Override
@@ -109,12 +183,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");
@@ -215,6 +289,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			}
 			cursor.close();
 		}
+		if (oldVersion < 15  && newVersion >= 15) {
+			recreateAxolotlDb(db);
+			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+					+ Message.FINGERPRINT + " TEXT");
+		}
 	}
 
 	public static synchronized DatabaseBackend getInstance(Context context) {
@@ -311,7 +390,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 				};
 		Cursor cursor = db.query(Conversation.TABLENAME, null,
 				Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
-						+ " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null);
+						+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
 		if (cursor.getCount() == 0)
 			return null;
 		cursor.moveToFirst();
@@ -481,4 +560,403 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		cursor.close();
 		return list;
 	}
+
+	private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
+		final SQLiteDatabase db = this.getReadableDatabase();
+		String[] columns = null;
+		String[] selectionArgs = {account.getUuid(),
+				contact.getName(),
+				Integer.toString(contact.getDeviceId())};
+		Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
+				columns,
+				AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+						+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+						+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ",
+				selectionArgs,
+				null, null, null);
+
+		return cursor;
+	}
+
+	public SessionRecord loadSession(Account account, AxolotlAddress contact) {
+		SessionRecord session = null;
+		Cursor cursor = getCursorForSession(account, contact);
+		if(cursor.getCount() != 0) {
+			cursor.moveToFirst();
+			try {
+				session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+			} catch (IOException e) {
+				cursor.close();
+				throw new AssertionError(e);
+			}
+		}
+		cursor.close();
+		return session;
+	}
+
+	public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
+		List<Integer> 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 + " = ?",
+				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, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
+		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(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+			} 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, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
+		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 = getCursorForSignedPreKey(account, signedPreKeyId);
+		if(cursor.getCount() != 0) {
+			cursor.moveToFirst();
+			try {
+				record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+			} catch (IOException e ) {
+				throw new AssertionError(e);
+			}
+		}
+		cursor.close();
+		return record;
+	}
+
+	public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
+		List<SignedPreKeyRecord> 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(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
+			} catch (IOException ignored) {
+			}
+		}
+		cursor.close();
+		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, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
+		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);
+	}
+
+	private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
+		return getIdentityKeyCursor(account, name, own, null);
+	}
+
+	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<String> selectionArgs = new ArrayList<>(4);
+		selectionArgs.add(account.getUuid());
+		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 + " = ?";
+		}
+		if (own != null){
+			selectionArgs.add(own?"1":"0");
+			selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?";
+		}
+		Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+				columns,
+				selectionString,
+				selectionArgs.toArray(new String[selectionArgs.size()]),
+				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, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
+			}
+		}
+		cursor.close();
+
+		return identityKeyPair;
+	}
+
+	public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
+		return loadIdentityKeys(account, name, null);
+	}
+
+	public Set<IdentityKey> loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) {
+		Set<IdentityKey> 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) {
+				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
+			}
+		}
+		cursor.close();
+
+		return identityKeys;
+	}
+
+	public long numTrustedKeys(Account account, String name) {
+		SQLiteDatabase db = getReadableDatabase();
+		String[] args = {
+				account.getUuid(),
+				name
+		};
+		return DatabaseUtils.queryNumEntries(db, AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+				AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"
+				+ " AND " + AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
+				args
+		);
+	}
+
+	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());
+		values.put(AxolotlService.SQLiteAxolotlStore.NAME, name);
+		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 fingerprint) {
+		Cursor cursor = getIdentityKeyCursor(account, 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 fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		String[] selectionArgs = {
+				account.getUuid(),
+				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.FINGERPRINT + " = ? ",
+				selectionArgs);
+		return rows == 1;
+	}
+
+	public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
+		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, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED);
+	}
+
+	public void recreateAxolotlDb() {
+		recreateAxolotlDb(getWritableDatabase());
+	}
+
+	public void recreateAxolotlDb(SQLiteDatabase db) {
+		Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
+		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);
+	}
+	
+	public void wipeAxolotlDb(Account account) {
+		String accountName = account.getUuid();
+		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> 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);
+	}
 }

src/main/java/eu/siacs/conversations/persistance/FileBackend.java πŸ”—

@@ -1,5 +1,19 @@
 package eu.siacs.conversations.persistance;
 
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
@@ -17,25 +31,11 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.Locale;
 
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.RectF;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.util.Base64;
-import android.util.Base64OutputStream;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExifHelper;

src/main/java/eu/siacs/conversations/services/EventReceiver.java πŸ”—

@@ -1,10 +1,11 @@
 package eu.siacs.conversations.services;
 
-import eu.siacs.conversations.persistance.DatabaseBackend;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 
+import eu.siacs.conversations.persistance.DatabaseBackend;
+
 public class EventReceiver extends BroadcastReceiver {
 	@Override
 	public void onReceive(Context context, Intent intent) {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -57,11 +57,11 @@ import eu.siacs.conversations.entities.Blockable;
 import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.generator.IqGenerator;
 import eu.siacs.conversations.generator.MessageGenerator;
 import eu.siacs.conversations.generator.PresenceGenerator;
@@ -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;
@@ -273,7 +274,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 					}
 				}
 				syncDirtyContacts(account);
-				scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
+				account.getAxolotlService().publishOwnDeviceIdIfNeeded();
+				account.getAxolotlService().publishBundlesIfNeeded();
+
+				scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
 			} else if (account.getStatus() == Account.State.OFFLINE) {
 				resetSendingToWaiting(account);
 				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
@@ -304,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;
@@ -591,9 +597,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);
@@ -699,7 +702,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);
@@ -753,6 +757,22 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 						}
 					}
 					break;
+				case Message.ENCRYPTION_AXOLOTL:
+					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;
+
 			}
 			if (packet != null) {
 				if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@@ -780,6 +800,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;
 			}
 		}
 
@@ -943,6 +966,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();
@@ -1342,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()) {
@@ -1372,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() {
@@ -1784,7 +1833,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) {
@@ -2260,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())) {

src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java πŸ”—

@@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener;
 import android.widget.AdapterView;
 import android.widget.ListView;
 
-import java.util.Set;
-import java.util.HashSet;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
-import java.util.ArrayList;
+import java.util.Set;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java πŸ”—

@@ -27,7 +27,6 @@ import org.openintents.openpgp.util.OpenPgpUtils;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.List;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
@@ -38,8 +37,8 @@ import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.User;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
 public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {

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;
 
@@ -362,13 +363,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) {
@@ -376,6 +377,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 				}
 			});
 		}
+		for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
+				contact.getAccount(), contact.getJid().toBareJid().toString())) {
+			hasKeys = true;
+			addFingerprintRow(keys, contact.getAccount(), identityKey);
+		}
 		if (contact.getPgpKeyId() != 0) {
 			hasKeys = true;
 			View view = inflater.inflate(R.layout.contact_key, keys, false);

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;
@@ -28,13 +29,15 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
 import android.widget.Toast;
 
 import net.java.otr4j.session.SessionStatus;
-import de.timroes.android.listview.EnhancedListView;
 
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
+import de.timroes.android.listview.EnhancedListView;
+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;
@@ -62,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";
@@ -76,6 +82,7 @@ public class ConversationActivity extends XmppActivity
 	final private List<Uri> mPendingImageUris = new ArrayList<>();
 	final private List<Uri> mPendingFileUris = new ArrayList<>();
 	private Uri mPendingGeoUri = null;
+	private boolean forbidProcessingPendings = false;
 
 	private View mContentView;
 
@@ -398,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() {
@@ -534,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);
+			}
 		}
 	}
 
@@ -749,6 +758,12 @@ public class ConversationActivity extends XmppActivity
 								showInstallPgpDialog();
 							}
 							break;
+						case R.id.encryption_choice_axolotl:
+							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);
 							break;
@@ -763,13 +778,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);
@@ -780,6 +800,10 @@ public class ConversationActivity extends XmppActivity
 				case Message.ENCRYPTION_PGP:
 					pgp.setChecked(true);
 					break;
+				case Message.ENCRYPTION_AXOLOTL:
+					popup.getMenu().findItem(R.id.encryption_choice_axolotl)
+							.setChecked(true);
+					break;
 				default:
 					none.setChecked(true);
 					break;
@@ -944,18 +968,23 @@ public class ConversationActivity extends XmppActivity
 			this.mConversationFragment.reInit(getSelectedConversation());
 		}
 
-		for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
-			attachImageToConversation(getSelectedConversation(),i.next());
-		}
+		if(!forbidProcessingPendings) {
+			for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
+				Uri foo = i.next();
+				attachImageToConversation(getSelectedConversation(), foo);
+			}
 
-		for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
-			attachFileToConversation(getSelectedConversation(),i.next());
-		}
+			for (Iterator<Uri> 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());
 	}
@@ -1065,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();
@@ -1217,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();

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;
@@ -46,12 +47,12 @@ import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
 import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
@@ -303,6 +304,10 @@ 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) {
+			if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
+				sendAxolotlMessage(message);
+			}
 		} else {
 			sendPlainTextMessage(message);
 		}
@@ -323,6 +328,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;
@@ -1120,6 +1128,13 @@ 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;
+		xmppService.sendMessage(message);
+		messageSent();
+	}
+
 	protected void sendOtrMessage(final Message message) {
 		final ConversationActivity activity = (ConversationActivity) getActivity();
 		final XmppConnectionService xmppService = activity.xmppConnectionService;
@@ -1182,4 +1197,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()));
+			}
+		}
+	}
+
 }

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java πŸ”—

@@ -1,6 +1,8 @@
 package eu.siacs.conversations.ui;
 
+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;
@@ -23,6 +25,10 @@ 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;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -54,9 +60,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	private TextView mServerInfoPep;
 	private TextView mSessionEst;
 	private TextView mOtrFingerprint;
+	private TextView mAxolotlFingerprint;
 	private ImageView mAvatar;
 	private RelativeLayout mOtrFingerprintBox;
+	private RelativeLayout mAxolotlFingerprintBox;
 	private ImageButton mOtrFingerprintToClipboardButton;
+	private ImageButton mAxolotlFingerprintToClipboardButton;
+	private ImageButton mRegenerateAxolotlKeyButton;
+	private LinearLayout keys;
+	private LinearLayout keysCard;
 
 	private Jid jidToEdit;
 	private Account mAccount;
@@ -310,6 +322,12 @@ 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_axolotl_to_clipboard);
+		this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
+		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);
@@ -338,6 +356,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 		final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
 		final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
 		final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
+		final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
 		if (mAccount != null && mAccount.isOnlineAndConnected()) {
 			if (!mAccount.getXmppConnection().getFeatures().blocking()) {
 				showBlocklist.setVisible(false);
@@ -345,6 +364,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			if (!mAccount.getXmppConnection().getFeatures().register()) {
 				changePassword.setVisible(false);
 			}
+			Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
+			if (otherDevices == null || otherDevices.isEmpty()) {
+				clearDevices.setVisible(false);
+			}
 		} else {
 			showQrCode.setVisible(false);
 			showBlocklist.setVisible(false);
@@ -415,6 +438,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 				changePasswordIntent.putExtra("account", mAccount.getJid().toString());
 				startActivity(changePasswordIntent);
 				break;
+			case R.id.action_clear_devices:
+				showWipePepDialog();
+				break;
 		}
 		return super.onOptionsItemSelected(item);
 	}
@@ -477,10 +503,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 +515,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 +526,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			} else {
 				this.mOtrFingerprintBox.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);
+			}
+			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()));
@@ -512,4 +587,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(getString(R.string.clear_other_devices));
+		builder.setIconAttribute(android.R.attr.alertDialogIcon);
+		builder.setMessage(getString(R.string.clear_other_devices_desc));
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setPositiveButton(getString(R.string.accept),
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						mAccount.getAxolotlService().wipeOtherPepDevices();
+					}
+				});
+		builder.create().show();
+	}
 }

src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java πŸ”—

@@ -1,27 +1,28 @@
 package eu.siacs.conversations.ui;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
-import eu.siacs.conversations.ui.adapter.AccountAdapter;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ContextMenu.ContextMenuInfo;
 import android.widget.AdapterView;
 import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ListView;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+
 public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {
 
 	protected Account selectedAccount = null;

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java πŸ”—

@@ -1,19 +1,6 @@
 package eu.siacs.conversations.ui;
 
-import java.security.KeyStoreException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Locale;
-
-import de.duenndns.ssl.MemorizingTrustManager;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.XmppConnection;
-
 import android.app.AlertDialog;
-import android.app.Fragment;
 import android.app.FragmentManager;
 import android.content.DialogInterface;
 import android.content.SharedPreferences;
@@ -25,6 +12,17 @@ import android.preference.Preference;
 import android.preference.PreferenceManager;
 import android.widget.Toast;
 
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
 public class SettingsActivity extends XmppActivity implements
 		OnSharedPreferenceChangeListener {
 	private SettingsFragment mSettingsFragment;

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java πŸ”—

@@ -0,0 +1,255 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+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.R;
+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 boolean hasOtherTrustedKeys = false;
+	private boolean hasPendingFetches = false;
+
+	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<IdentityKey, Boolean> ownKeysToTrust = new HashMap<>();
+	private final Map<IdentityKey, Boolean> 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);
+		}
+		if(hasPendingFetches) {
+			setFetching();
+			lock();
+		} else {
+			if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){
+				lock();
+			} else {
+				unlock();
+			}
+			setDone();
+		}
+	}
+
+	private void getFingerprints(final Account account) {
+		Set<IdentityKey> ownKeysSet = account.getAxolotlService().getPendingKeys();
+		for(final IdentityKey identityKey : ownKeysSet) {
+			if(!ownKeysToTrust.containsKey(identityKey)) {
+				ownKeysToTrust.put(identityKey, false);
+			}
+		}
+		Set<IdentityKey> 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);
+
+			if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) {
+				hasOtherTrustedKeys = true;
+			}
+			Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false);
+			if(account.getAxolotlService().hasPendingKeyFetches(conversation)) {
+				hasPendingFetches = true;
+			}
+
+			populateView();
+		}
+	}
+
+	@Override
+	public void onNewKeysAvailable() {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				final Account account = xmppConnectionService
+						.findAccountByJid(accountJid);
+				hasPendingFetches = false;
+				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.setTextColor(getPrimaryTextColor());
+	}
+
+	private void lock() {
+		mSaveButton.setEnabled(false);
+		mSaveButton.setTextColor(getSecondaryTextColor());
+	}
+
+	private void setDone() {
+		mSaveButton.setText(getString(R.string.done));
+	}
+
+	private void setFetching() {
+		mSaveButton.setText(getString(R.string.fetching_keys));
+	}
+}

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;
@@ -56,6 +59,8 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
 
 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 +70,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,7 +80,10 @@ 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.OnNewKeysAvailable;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
@@ -287,6 +296,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() {
@@ -308,6 +320,9 @@ public abstract class XmppActivity extends Activity {
 		if (this instanceof XmppConnectionService.OnShowErrorToast) {
 			this.xmppConnectionService.removeOnShowErrorToastListener();
 		}
+		if (this instanceof OnNewKeysAvailable) {
+			this.xmppConnectionService.removeOnNewKeysAvailableListener();
+		}
 	}
 
 	@Override
@@ -443,7 +458,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);
@@ -588,6 +603,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();

src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java πŸ”—

@@ -1,13 +1,5 @@
 package eu.siacs.conversations.ui.adapter;
 
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.ui.ManageAccountActivity;
-import eu.siacs.conversations.ui.widget.Switch;
-
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -17,6 +9,14 @@ import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.widget.Switch;
+
 public class AccountAdapter extends ArrayAdapter<Account> {
 
 	private XmppActivity activity;

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java πŸ”—

@@ -21,8 +21,8 @@ import java.util.concurrent.RejectedExecutionException;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.utils.UIHelper;

src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java πŸ”—

@@ -1,13 +1,13 @@
 package eu.siacs.conversations.ui.adapter;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
 import android.content.Context;
 import android.widget.ArrayAdapter;
 import android.widget.Filter;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
 public class KnownHostsAdapter extends ArrayAdapter<String> {
 	private ArrayList<String> domains;
 	private Filter domainFilter = new Filter() {

src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java πŸ”—

@@ -1,15 +1,5 @@
 package eu.siacs.conversations.ui.adapter;
 
-import java.lang.ref.WeakReference;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.utils.UIHelper;
-import eu.siacs.conversations.xmpp.jid.Jid;
-
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
@@ -26,6 +16,16 @@ import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
 public class ListItemAdapter extends ArrayAdapter<ListItem> {
 
 	protected XmppActivity activity;

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,13 +27,14 @@ 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;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Message.FileParams;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.UIHelper;
@@ -154,6 +156,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			viewHolder.indicator.setVisibility(View.GONE);
 		} else {
 			viewHolder.indicator.setVisibility(View.VISIBLE);
+			if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+				AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation()
+						.getAccount().getAxolotlService().getFingerprintTrust(
+								message.getAxolotlFingerprint());
+
+				if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) {
+					viewHolder.indicator.setColorFilter(Color.RED);
+				} else {
+					viewHolder.indicator.clearColorFilter();
+				}
+			}
 		}
 
 		String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),

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<builder.length();i+=9) {
+			builder.insert(i, ' ');
+		}
 		return builder.toString();
 	}
 

src/main/java/eu/siacs/conversations/utils/DNSHelper.java πŸ”—

@@ -1,17 +1,7 @@
 package eu.siacs.conversations.utils;
 
-import de.measite.minidns.Client;
-import de.measite.minidns.DNSMessage;
-import de.measite.minidns.Record;
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.Record.CLASS;
-import de.measite.minidns.record.SRV;
-import de.measite.minidns.record.A;
-import de.measite.minidns.record.AAAA;
-import de.measite.minidns.record.Data;
-import de.measite.minidns.util.NameUtil;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.jid.Jid;
+import android.os.Bundle;
+import android.util.Log;
 
 import java.io.IOException;
 import java.net.InetAddress;
@@ -22,8 +12,18 @@ import java.util.Random;
 import java.util.TreeMap;
 import java.util.regex.Pattern;
 
-import android.os.Bundle;
-import android.util.Log;
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.jid.Jid;
 
 public class DNSHelper {
 

src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java πŸ”—

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.utils;
 
+import android.content.Context;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -8,8 +10,6 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.Thread.UncaughtExceptionHandler;
 
-import android.content.Context;
-
 public class ExceptionHandler implements UncaughtExceptionHandler {
 
 	private UncaughtExceptionHandler defaultHandler;

src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java πŸ”—

@@ -1,5 +1,17 @@
 package eu.siacs.conversations.utils;
 
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+
 import java.io.BufferedReader;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -15,18 +27,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.content.DialogInterface.OnClickListener;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.preference.PreferenceManager;
-import android.text.format.DateUtils;
-import android.util.Log;
-
 public class ExceptionHelper {
 	public static void init(Context context) {
 		if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {

src/main/java/eu/siacs/conversations/utils/PhoneHelper.java πŸ”—

@@ -1,9 +1,5 @@
 package eu.siacs.conversations.utils;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
 import android.content.Context;
 import android.content.CursorLoader;
 import android.content.Loader;
@@ -15,6 +11,9 @@ import android.os.Bundle;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Profile;
 
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
 public class PhoneHelper {
 
 	public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {

src/main/java/eu/siacs/conversations/utils/UIHelper.java πŸ”—

@@ -1,5 +1,10 @@
 package eu.siacs.conversations.utils;
 
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Pair;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -9,15 +14,10 @@ import java.util.Locale;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
-import android.content.Context;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.util.Pair;
-
 public class UIHelper {
 
 	private static String BLACK_HEART_SUIT = "\u2665";

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);

src/main/java/eu/siacs/conversations/xml/XmlReader.java πŸ”—

@@ -1,18 +1,18 @@
 package eu.siacs.conversations.xml;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import eu.siacs.conversations.Config;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.util.Log;
-import android.util.Xml;
+import eu.siacs.conversations.Config;
 
 public class XmlReader {
 	private XmlPullParser parser;

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -26,7 +26,6 @@ import java.net.IDN;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
-import java.net.SocketAddress;
 import java.net.UnknownHostException;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java πŸ”—

@@ -1,5 +1,10 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.content.Intent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Log;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -8,17 +13,13 @@ import java.util.Locale;
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 
-import android.content.Intent;
-import android.net.Uri;
-import android.os.SystemClock;
-import android.util.Log;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java πŸ”—

@@ -1,16 +1,18 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.annotation.SuppressLint;
+import android.util.Log;
+
 import java.math.BigInteger;
 import java.security.SecureRandom;
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
-import android.annotation.SuppressLint;
-import android.util.Log;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Xmlns;

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java πŸ”—

@@ -1,5 +1,8 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.util.Base64;
+import android.util.Log;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -7,9 +10,6 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 
-import android.util.Base64;
-import android.util.Log;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;

src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java πŸ”—

@@ -1,10 +1,10 @@
 package eu.siacs.conversations.xmpp.pep;
 
+import android.util.Base64;
+
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
-import android.util.Base64;
-
 public class Avatar {
 
 	public enum Origin { PEP, VCARD };

src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java πŸ”—

@@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.stanzas;
 
 import android.util.Pair;
 
-import java.text.ParseException;
-
 import eu.siacs.conversations.parser.AbstractParser;
 import eu.siacs.conversations.xml.Element;
 
@@ -29,6 +27,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:

src/main/res/layout/activity_edit_account.xml πŸ”—

@@ -342,6 +342,95 @@
                         android:visibility="visible"
                         android:contentDescription="@string/copy_otr_clipboard_description"/>
                 </RelativeLayout>
+               <RelativeLayout
+                   android:layout_width="wrap_content"
+                   android:layout_height="match_parent"
+                   android:id="@+id/axolotl_fingerprint_box"
+                   android:layout_marginTop="32dp">
+
+                   <LinearLayout
+                       android:layout_width="wrap_content"
+                       android:layout_height="wrap_content"
+                       android:layout_alignParentLeft="true"
+                       android:layout_toLeftOf="@+id/axolotl_actions"
+                       android:orientation="vertical">
+
+                       <TextView
+                           android:id="@+id/axolotl_fingerprint"
+                           android:layout_width="wrap_content"
+                           android:layout_height="wrap_content"
+                           android:textColor="@color/black87"
+                           android:textSize="?attr/TextSizeBody"
+                           android:typeface="monospace" />
+
+                       <TextView
+                           android:layout_width="wrap_content"
+                           android:layout_height="wrap_content"
+                           android:textColor="@color/black54"
+                           android:textSize="?attr/TextSizeInfo"
+                           android:text="@string/this_device_axolotl_fingerprint"/>
+                   </LinearLayout>
+
+                   <LinearLayout
+                       android:layout_width="wrap_content"
+                       android:layout_height="wrap_content"
+                       android:id="@+id/axolotl_actions"
+                       android:layout_centerVertical="true"
+                       android:layout_alignParentRight="true"
+                       android:orientation="vertical">
+
+                       <ImageButton
+                           android:id="@+id/action_copy_axolotl_to_clipboard"
+                           android:layout_width="wrap_content"
+                           android:layout_height="wrap_content"
+                           android:background="?android:selectableItemBackground"
+                           android:padding="@dimen/image_button_padding"
+                           android:src="?attr/icon_copy"
+                           android:visibility="visible"
+                           android:contentDescription="@string/copy_axolotl_clipboard_description"/>
+                       <ImageButton
+                           android:id="@+id/action_regenerate_axolotl_key"
+                           android:layout_width="wrap_content"
+                           android:layout_height="wrap_content"
+                           android:background="?android:selectableItemBackground"
+                           android:padding="@dimen/image_button_padding"
+                           android:src="?attr/icon_refresh"
+                           android:visibility="visible"
+                           android:contentDescription="@string/regenerate_axolotl_key"/>
+
+                   </LinearLayout>
+               </RelativeLayout>
+            </LinearLayout>
+            <LinearLayout
+                android:id="@+id/other_device_keys_card"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                android:layout_marginRight="@dimen/activity_horizontal_margin"
+                android:layout_marginTop="@dimen/activity_vertical_margin"
+                android:layout_marginBottom="@dimen/activity_vertical_margin"
+                android:background="@drawable/infocard_border"
+                android:orientation="vertical"
+                android:padding="@dimen/infocard_padding"
+                android:visibility="gone">
+
+                <TextView
+                    android:id="@+id/other_device_keys_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/black87"
+                    android:textSize="?attr/TextSizeHeadline"
+                    android:textStyle="bold"
+                    android:text="@string/other_devices"/>
+
+                <LinearLayout
+                    android:id="@+id/other_device_keys"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:divider="?android:dividerHorizontal"
+                    android:orientation="vertical"
+                    android:showDividers="middle" >
+                </LinearLayout>
             </LinearLayout>
         </LinearLayout>
     </ScrollView>

src/main/res/layout/activity_trust_keys.xml πŸ”—

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/grey200" >
+    <ScrollView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_above="@+id/button_bar"
+        android:layout_alignParentTop="true" >
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:id="@+id/own_keys_card"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                android:layout_marginRight="@dimen/activity_horizontal_margin"
+                android:layout_marginTop="@dimen/activity_vertical_margin"
+                android:layout_marginBottom="@dimen/activity_vertical_margin"
+                android:background="@drawable/infocard_border"
+                android:orientation="vertical"
+                android:padding="@dimen/infocard_padding"
+                android:visibility="gone">
+
+                <TextView
+                    android:id="@+id/own_keys_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/black87"
+                    android:textSize="?attr/TextSizeHeadline"
+                    android:textStyle="bold"/>
+
+                <LinearLayout
+                    android:id="@+id/own_keys_details"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:divider="?android:dividerHorizontal"
+                    android:showDividers="middle"
+                    android:orientation="vertical">
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/foreign_keys_card"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                android:layout_marginRight="@dimen/activity_horizontal_margin"
+                android:layout_marginTop="@dimen/activity_vertical_margin"
+                android:layout_marginBottom="@dimen/activity_vertical_margin"
+                android:background="@drawable/infocard_border"
+                android:orientation="vertical"
+                android:padding="@dimen/infocard_padding"
+                android:visibility="gone">
+
+                <TextView
+                    android:id="@+id/foreign_keys_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/black87"
+                    android:textSize="?attr/TextSizeHeadline"
+                    android:textStyle="bold"/>
+
+                <LinearLayout
+                    android:id="@+id/foreign_keys_details"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:divider="?android:dividerHorizontal"
+                    android:showDividers="middle"
+                    android:orientation="vertical">
+                </LinearLayout>
+
+            </LinearLayout>
+
+        </LinearLayout>
+    </ScrollView>
+    <LinearLayout
+        android:id="@+id/button_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentEnd="true"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentRight="true" >
+
+        <Button
+            android:id="@+id/cancel_button"
+            style="?android:attr/borderlessButtonStyle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/cancel"
+            android:textColor="@color/black87" />
+
+        <View
+            android:layout_width="1dp"
+            android:layout_height="fill_parent"
+            android:layout_marginBottom="7dp"
+            android:layout_marginTop="7dp"
+            android:background="@color/black12" />
+
+        <Button
+            android:id="@+id/save_button"
+            style="?android:attr/borderlessButtonStyle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:enabled="true"
+            android:textColor="@color/black54"
+            android:text="@string/done"/>
+    </LinearLayout>
+</RelativeLayout>

src/main/res/layout/contact_key.xml πŸ”—

@@ -3,12 +3,11 @@
     android:layout_width="wrap_content"
     android:layout_height="match_parent" >
 
-    <LinearLayout
+    <RelativeLayout
+        android:id="@+id/key_data"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentLeft="true"
-        android:layout_toLeftOf="@+id/button_remove"
-        android:orientation="vertical"
         android:padding="8dp" >
 
         <TextView
@@ -16,6 +15,8 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textColor="@color/black87"
+            android:layout_alignParentLeft="true"
+            android:layout_toLeftOf="@+id/tgl_trust"
             android:textSize="?attr/TextSizeBody"
             android:typeface="monospace" />
 
@@ -24,18 +25,41 @@
             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"/>
+
+        <TextView
+            android:id="@+id/key_trust"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_below="@+id/key"
+            android:visibility="gone"
+            android:textColor="@color/black54"
             android:textSize="?attr/TextSizeInfo"/>
-    </LinearLayout>
 
     <ImageButton
         android:id="@+id/button_remove"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
+        android:layout_toRightOf="@+id/key"
         android:layout_centerVertical="true"
         android:background="?android:selectableItemBackground"
         android:padding="@dimen/image_button_padding"
         android:src="?attr/icon_remove"
-        android:visibility="invisible" />
+        android:visibility="gone" />
+
+
+    <eu.siacs.conversations.ui.widget.Switch
+        android:id="@+id/tgl_trust"
+        android:visibility="invisible"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        style="@style/MD"/>
 
+    </RelativeLayout>
 </RelativeLayout>

src/main/res/menu/editaccount.xml πŸ”—

@@ -20,4 +20,8 @@
 	<item android:id="@+id/action_change_password_on_server"
           android:title="@string/change_password"
           android:showAsAction="never" />
+
+    <item android:id="@+id/action_clear_devices"
+          android:title="@string/clear_other_devices"
+          android:showAsAction="never"/>
 </menu>

src/main/res/menu/encryption_choices.xml πŸ”—

@@ -11,6 +11,9 @@
         <item
             android:id="@+id/encryption_choice_pgp"
             android:title="@string/encryption_choice_pgp"/>
+        <item
+            android:id="@+id/encryption_choice_axolotl"
+            android:title="@string/encryption_choice_axolotl"/>
     </group>
 
 </menu>

src/main/res/values-v21/themes.xml πŸ”—

@@ -21,8 +21,10 @@
         <item name="attr/icon_download">@drawable/ic_file_download_white_24dp</item>
         <item name="attr/icon_edit">@drawable/ic_edit_white_24dp</item>
         <item name="attr/icon_edit_dark">@drawable/ic_edit_grey600_24dp</item>
+        <item name="attr/icon_done">@drawable/ic_done_black_24dp</item>
         <item name="attr/icon_group">@drawable/ic_group_white_24dp</item>
         <item name="attr/icon_new">@drawable/ic_add_white_24dp</item>
+        <item name="attr/icon_refresh">@drawable/ic_refresh_grey600_24dp</item>
         <item name="attr/icon_new_attachment">@drawable/ic_attach_file_white_24dp</item>
         <item name="attr/icon_not_secure">@drawable/ic_lock_open_white_24dp</item>
         <item name="attr/icon_remove">@drawable/ic_delete_grey600_24dp</item>

src/main/res/values/attrs.xml πŸ”—

@@ -14,6 +14,7 @@
     <attr name="icon_download" format="reference"/>
     <attr name="icon_edit" format="reference"/>
     <attr name="icon_edit_dark" format="reference"/>
+    <attr name="icon_done" format="reference"/>
     <attr name="icon_group" format="reference"/>
     <attr name="icon_new" format="reference"/>
     <attr name="icon_new_attachment" format="reference"/>

src/main/res/values/strings.xml πŸ”—

@@ -80,6 +80,7 @@
 	<string name="choose_presence">Choose presence to contact</string>
 	<string name="send_plain_text_message">Send plain text message</string>
 	<string name="send_otr_message">Send OTR encrypted message</string>
+	<string name="send_axolotl_message">Send Axolotl encrypted message</string>
 	<string name="send_pgp_message">Send OpenPGP encrypted message</string>
 	<string name="your_nick_has_been_changed">Your nickname has been changed</string>
 	<string name="send_unencrypted">Send unencrypted</string>
@@ -154,6 +155,7 @@
 	<string name="encryption_choice_none">Plain text</string>
 	<string name="encryption_choice_otr">OTR</string>
 	<string name="encryption_choice_pgp">OpenPGP</string>
+	<string name="encryption_choice_axolotl">Axolotl</string>
 	<string name="mgmt_account_edit">Edit account</string>
 	<string name="mgmt_account_delete">Delete account</string>
 	<string name="mgmt_account_disable">Temporarily disable</string>
@@ -206,6 +208,13 @@
 	<string name="reception_failed">Reception failed</string>
 	<string name="your_fingerprint">Your fingerprint</string>
 	<string name="otr_fingerprint">OTR fingerprint</string>
+	<string name="axolotl_fingerprint">Axolotl fingerprint</string>
+	<string name="this_device_axolotl_fingerprint">Own Axolotl fingerprint</string>
+	<string name="other_devices">Other devices</string>
+	<string name="trust_keys">Trust Axolotl Keys</string>
+	<string name="fetching_keys">Fetching keys...</string>
+	<string name="done">Done</string>
+	<string name="axolotl_devicelist">Other own Axolotl Devices</string>
 	<string name="verify">Verify</string>
 	<string name="decrypt">Decrypt</string>
 	<string name="conferences">Conferences</string>
@@ -312,6 +321,7 @@
 	<string name="pref_conference_name">Conference name</string>
 	<string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string>
 	<string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
+	<string name="toast_message_axolotl_fingerprint">Axolotl fingerprint copied to clipboard!</string>
 	<string name="conference_banned">You are banned from this conference</string>
 	<string name="conference_members_only">This conference is members only</string>
 	<string name="conference_kicked">You have been kicked from this conference</string>
@@ -378,6 +388,11 @@
 	<string name="reset">Reset</string>
 	<string name="account_image_description">Account avatar</string>
 	<string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string>
+	<string name="copy_axolotl_clipboard_description">Copy Axolotl fingerprint to clipboard</string>
+	<string name="regenerate_axolotl_key">Copy Axolotl fingerprint to clipboard</string>
+	<string name="wipe_axolotl_pep">Wipe other devices from PEP</string>
+	<string name="clear_other_devices">Clear devices</string>
+	<string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the axolotl announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string>
 	<string name="fetching_history_from_server">Fetching history from server</string>
 	<string name="no_more_history_on_server">No more history on server</string>
 	<string name="updating">Updating…</string>

src/main/res/values/themes.xml πŸ”—

@@ -18,6 +18,7 @@
         <item name="attr/icon_download">@drawable/ic_action_download</item>
         <item name="attr/icon_edit">@drawable/ic_action_edit</item>
         <item name="attr/icon_edit_dark">@drawable/ic_action_edit_dark</item>
+        <item name="attr/icon_done">@drawable/ic_action_done</item>
 
         <item name="attr/icon_group">@drawable/ic_action_group</item>
         <item name="attr/icon_new">@drawable/ic_action_new</item>