AxolotlService.java

   1package eu.siacs.conversations.crypto.axolotl;
   2
   3import android.support.annotation.NonNull;
   4import android.support.annotation.Nullable;
   5import android.util.Base64;
   6import android.util.Log;
   7
   8import org.whispersystems.libaxolotl.AxolotlAddress;
   9import org.whispersystems.libaxolotl.DuplicateMessageException;
  10import org.whispersystems.libaxolotl.IdentityKey;
  11import org.whispersystems.libaxolotl.IdentityKeyPair;
  12import org.whispersystems.libaxolotl.InvalidKeyException;
  13import org.whispersystems.libaxolotl.InvalidKeyIdException;
  14import org.whispersystems.libaxolotl.InvalidMessageException;
  15import org.whispersystems.libaxolotl.InvalidVersionException;
  16import org.whispersystems.libaxolotl.LegacyMessageException;
  17import org.whispersystems.libaxolotl.NoSessionException;
  18import org.whispersystems.libaxolotl.SessionBuilder;
  19import org.whispersystems.libaxolotl.SessionCipher;
  20import org.whispersystems.libaxolotl.UntrustedIdentityException;
  21import org.whispersystems.libaxolotl.ecc.Curve;
  22import org.whispersystems.libaxolotl.ecc.ECKeyPair;
  23import org.whispersystems.libaxolotl.ecc.ECPublicKey;
  24import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
  25import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
  26import org.whispersystems.libaxolotl.protocol.WhisperMessage;
  27import org.whispersystems.libaxolotl.state.AxolotlStore;
  28import org.whispersystems.libaxolotl.state.PreKeyBundle;
  29import org.whispersystems.libaxolotl.state.PreKeyRecord;
  30import org.whispersystems.libaxolotl.state.SessionRecord;
  31import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
  32import org.whispersystems.libaxolotl.util.KeyHelper;
  33
  34import java.util.ArrayList;
  35import java.util.Arrays;
  36import java.util.HashMap;
  37import java.util.HashSet;
  38import java.util.List;
  39import java.util.Map;
  40import java.util.Random;
  41import java.util.Set;
  42
  43import eu.siacs.conversations.Config;
  44import eu.siacs.conversations.entities.Account;
  45import eu.siacs.conversations.entities.Contact;
  46import eu.siacs.conversations.entities.Conversation;
  47import eu.siacs.conversations.entities.Message;
  48import eu.siacs.conversations.parser.IqParser;
  49import eu.siacs.conversations.services.XmppConnectionService;
  50import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
  51import eu.siacs.conversations.xml.Element;
  52import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  53import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  54import eu.siacs.conversations.xmpp.jid.Jid;
  55import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  56import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  57
  58public class AxolotlService {
  59
  60	public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
  61	public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
  62	public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
  63
  64	public static final String LOGPREFIX = "AxolotlService";
  65
  66	public static final int NUM_KEYS_TO_PUBLISH = 10;
  67
  68	private final Account account;
  69	private final XmppConnectionService mXmppConnectionService;
  70	private final SQLiteAxolotlStore axolotlStore;
  71	private final SessionMap sessions;
  72	private final Map<Jid, Set<Integer>> deviceIds;
  73	private final Map<String, MessagePacket> messageCache;
  74	private final FetchStatusMap fetchStatusMap;
  75	private final SerialSingleThreadExecutor executor;
  76
  77	public static class SQLiteAxolotlStore implements AxolotlStore {
  78
  79		public static final String PREKEY_TABLENAME = "prekeys";
  80		public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
  81		public static final String SESSION_TABLENAME = "sessions";
  82		public static final String IDENTITIES_TABLENAME = "identities";
  83		public static final String ACCOUNT = "account";
  84		public static final String DEVICE_ID = "device_id";
  85		public static final String ID = "id";
  86		public static final String KEY = "key";
  87		public static final String FINGERPRINT = "fingerprint";
  88		public static final String NAME = "name";
  89		public static final String TRUSTED = "trusted";
  90		public static final String OWN = "ownkey";
  91
  92		public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
  93		public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
  94
  95		private final Account account;
  96		private final XmppConnectionService mXmppConnectionService;
  97
  98		private IdentityKeyPair identityKeyPair;
  99		private int localRegistrationId;
 100		private int currentPreKeyId = 0;
 101
 102		public enum Trust {
 103			UNDECIDED, // 0
 104			TRUSTED,
 105			UNTRUSTED;
 106
 107			public String toString() {
 108				switch(this){
 109					case UNDECIDED:
 110						return "Trust undecided";
 111					case TRUSTED:
 112						return "Trusted";
 113					case UNTRUSTED:
 114					default:
 115						return "Untrusted";
 116				}
 117			}
 118
 119			public static Trust fromBoolean(Boolean trusted) {
 120				return trusted?TRUSTED:UNTRUSTED;
 121			}
 122		};
 123
 124		private static IdentityKeyPair generateIdentityKeyPair() {
 125			Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair...");
 126			ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
 127			IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
 128					identityKeyPairKeys.getPrivateKey());
 129			return ownKey;
 130		}
 131
 132		private static int generateRegistrationId() {
 133			Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID...");
 134			int reg_id = KeyHelper.generateRegistrationId(true);
 135			return reg_id;
 136		}
 137
 138		public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
 139			this.account = account;
 140			this.mXmppConnectionService = service;
 141			this.localRegistrationId = loadRegistrationId();
 142			this.currentPreKeyId = loadCurrentPreKeyId();
 143			for (SignedPreKeyRecord record : loadSignedPreKeys()) {
 144				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId());
 145			}
 146		}
 147
 148		public int getCurrentPreKeyId() {
 149			return currentPreKeyId;
 150		}
 151
 152		// --------------------------------------
 153		// IdentityKeyStore
 154		// --------------------------------------
 155
 156		private IdentityKeyPair loadIdentityKeyPair() {
 157			String ownName = account.getJid().toBareJid().toString();
 158			IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
 159					ownName);
 160
 161			if (ownKey != null) {
 162				return ownKey;
 163			} else {
 164				Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName);
 165				ownKey = generateIdentityKeyPair();
 166				mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
 167			}
 168			return ownKey;
 169		}
 170
 171		private int loadRegistrationId() {
 172			return loadRegistrationId(false);
 173		}
 174
 175		private int loadRegistrationId(boolean regenerate) {
 176			String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
 177			int reg_id;
 178			if (!regenerate && regIdString != null) {
 179				reg_id = Integer.valueOf(regIdString);
 180			} else {
 181				Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid());
 182				reg_id = generateRegistrationId();
 183				boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
 184				if (success) {
 185					mXmppConnectionService.databaseBackend.updateAccount(account);
 186				} else {
 187					Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new key to the database!");
 188				}
 189			}
 190			return reg_id;
 191		}
 192
 193		private int loadCurrentPreKeyId() {
 194			String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
 195			int reg_id;
 196			if (regIdString != null) {
 197				reg_id = Integer.valueOf(regIdString);
 198			} else {
 199				Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid());
 200				reg_id = 0;
 201			}
 202			return reg_id;
 203		}
 204
 205		public void regenerate() {
 206			mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
 207			account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
 208			identityKeyPair = loadIdentityKeyPair();
 209			localRegistrationId = loadRegistrationId(true);
 210			currentPreKeyId = 0;
 211			mXmppConnectionService.updateAccountUi();
 212		}
 213
 214		/**
 215		 * Get the local client's identity key pair.
 216		 *
 217		 * @return The local client's persistent identity key pair.
 218		 */
 219		@Override
 220		public IdentityKeyPair getIdentityKeyPair() {
 221			if(identityKeyPair == null) {
 222				identityKeyPair = loadIdentityKeyPair();
 223			}
 224			return identityKeyPair;
 225		}
 226
 227		/**
 228		 * Return the local client's registration ID.
 229		 * <p/>
 230		 * Clients should maintain a registration ID, a random number
 231		 * between 1 and 16380 that's generated once at install time.
 232		 *
 233		 * @return the local client's registration ID.
 234		 */
 235		@Override
 236		public int getLocalRegistrationId() {
 237			return localRegistrationId;
 238		}
 239
 240		/**
 241		 * Save a remote client's identity key
 242		 * <p/>
 243		 * Store a remote client's identity key as trusted.
 244		 *
 245		 * @param name        The name of the remote client.
 246		 * @param identityKey The remote client's identity key.
 247		 */
 248		@Override
 249		public void saveIdentity(String name, IdentityKey identityKey) {
 250			if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
 251				mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
 252			}
 253		}
 254
 255		/**
 256		 * Verify a remote client's identity key.
 257		 * <p/>
 258		 * Determine whether a remote client's identity is trusted.  Convention is
 259		 * that the TextSecure protocol is 'trust on first use.'  This means that
 260		 * an identity key is considered 'trusted' if there is no entry for the recipient
 261		 * in the local store, or if it matches the saved key for a recipient in the local
 262		 * store.  Only if it mismatches an entry in the local store is it considered
 263		 * 'untrusted.'
 264		 *
 265		 * @param name        The name of the remote client.
 266		 * @param identityKey The identity key to verify.
 267		 * @return true if trusted, false if untrusted.
 268		 */
 269		@Override
 270		public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
 271			return true;
 272		}
 273
 274		public Trust getFingerprintTrust(String fingerprint) {
 275			return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
 276		}
 277
 278		public void setFingerprintTrust(String fingerprint, Trust trust) {
 279			mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
 280		}
 281
 282		public Set<IdentityKey> getContactUndecidedKeys(String bareJid) {
 283			return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, Trust.UNDECIDED);
 284		}
 285
 286		// --------------------------------------
 287		// SessionStore
 288		// --------------------------------------
 289
 290		/**
 291		 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
 292		 * or a new SessionRecord if one does not currently exist.
 293		 * <p/>
 294		 * It is important that implementations return a copy of the current durable information.  The
 295		 * returned SessionRecord may be modified, but those changes should not have an effect on the
 296		 * durable session state (what is returned by subsequent calls to this method) without the
 297		 * store method being called here first.
 298		 *
 299		 * @param address The name and device ID of the remote client.
 300		 * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
 301		 * a new SessionRecord if one does not currently exist.
 302		 */
 303		@Override
 304		public SessionRecord loadSession(AxolotlAddress address) {
 305			SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
 306			return (session != null) ? session : new SessionRecord();
 307		}
 308
 309		/**
 310		 * Returns all known devices with active sessions for a recipient
 311		 *
 312		 * @param name the name of the client.
 313		 * @return all known sub-devices with active sessions.
 314		 */
 315		@Override
 316		public List<Integer> getSubDeviceSessions(String name) {
 317			return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
 318					new AxolotlAddress(name, 0));
 319		}
 320
 321		/**
 322		 * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
 323		 *
 324		 * @param address the address of the remote client.
 325		 * @param record  the current SessionRecord for the remote client.
 326		 */
 327		@Override
 328		public void storeSession(AxolotlAddress address, SessionRecord record) {
 329			mXmppConnectionService.databaseBackend.storeSession(account, address, record);
 330		}
 331
 332		/**
 333		 * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
 334		 *
 335		 * @param address the address of the remote client.
 336		 * @return true if a {@link SessionRecord} exists, false otherwise.
 337		 */
 338		@Override
 339		public boolean containsSession(AxolotlAddress address) {
 340			return mXmppConnectionService.databaseBackend.containsSession(account, address);
 341		}
 342
 343		/**
 344		 * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
 345		 *
 346		 * @param address the address of the remote client.
 347		 */
 348		@Override
 349		public void deleteSession(AxolotlAddress address) {
 350			mXmppConnectionService.databaseBackend.deleteSession(account, address);
 351		}
 352
 353		/**
 354		 * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
 355		 *
 356		 * @param name the name of the remote client.
 357		 */
 358		@Override
 359		public void deleteAllSessions(String name) {
 360			mXmppConnectionService.databaseBackend.deleteAllSessions(account,
 361					new AxolotlAddress(name, 0));
 362		}
 363
 364		// --------------------------------------
 365		// PreKeyStore
 366		// --------------------------------------
 367
 368		/**
 369		 * Load a local PreKeyRecord.
 370		 *
 371		 * @param preKeyId the ID of the local PreKeyRecord.
 372		 * @return the corresponding PreKeyRecord.
 373		 * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
 374		 */
 375		@Override
 376		public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
 377			PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
 378			if (record == null) {
 379				throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
 380			}
 381			return record;
 382		}
 383
 384		/**
 385		 * Store a local PreKeyRecord.
 386		 *
 387		 * @param preKeyId the ID of the PreKeyRecord to store.
 388		 * @param record   the PreKeyRecord.
 389		 */
 390		@Override
 391		public void storePreKey(int preKeyId, PreKeyRecord record) {
 392			mXmppConnectionService.databaseBackend.storePreKey(account, record);
 393			currentPreKeyId = preKeyId;
 394			boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
 395			if (success) {
 396				mXmppConnectionService.databaseBackend.updateAccount(account);
 397			} else {
 398				Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new prekey id to the database!");
 399			}
 400		}
 401
 402		/**
 403		 * @param preKeyId A PreKeyRecord ID.
 404		 * @return true if the store has a record for the preKeyId, otherwise false.
 405		 */
 406		@Override
 407		public boolean containsPreKey(int preKeyId) {
 408			return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
 409		}
 410
 411		/**
 412		 * Delete a PreKeyRecord from local storage.
 413		 *
 414		 * @param preKeyId The ID of the PreKeyRecord to remove.
 415		 */
 416		@Override
 417		public void removePreKey(int preKeyId) {
 418			mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
 419		}
 420
 421		// --------------------------------------
 422		// SignedPreKeyStore
 423		// --------------------------------------
 424
 425		/**
 426		 * Load a local SignedPreKeyRecord.
 427		 *
 428		 * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
 429		 * @return the corresponding SignedPreKeyRecord.
 430		 * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
 431		 */
 432		@Override
 433		public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
 434			SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
 435			if (record == null) {
 436				throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
 437			}
 438			return record;
 439		}
 440
 441		/**
 442		 * Load all local SignedPreKeyRecords.
 443		 *
 444		 * @return All stored SignedPreKeyRecords.
 445		 */
 446		@Override
 447		public List<SignedPreKeyRecord> loadSignedPreKeys() {
 448			return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
 449		}
 450
 451		/**
 452		 * Store a local SignedPreKeyRecord.
 453		 *
 454		 * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
 455		 * @param record         the SignedPreKeyRecord.
 456		 */
 457		@Override
 458		public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
 459			mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
 460		}
 461
 462		/**
 463		 * @param signedPreKeyId A SignedPreKeyRecord ID.
 464		 * @return true if the store has a record for the signedPreKeyId, otherwise false.
 465		 */
 466		@Override
 467		public boolean containsSignedPreKey(int signedPreKeyId) {
 468			return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
 469		}
 470
 471		/**
 472		 * Delete a SignedPreKeyRecord from local storage.
 473		 *
 474		 * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
 475		 */
 476		@Override
 477		public void removeSignedPreKey(int signedPreKeyId) {
 478			mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
 479		}
 480	}
 481
 482	public static class XmppAxolotlSession {
 483		private final SessionCipher cipher;
 484		private Integer preKeyId = null;
 485		private final SQLiteAxolotlStore sqLiteAxolotlStore;
 486		private final AxolotlAddress remoteAddress;
 487		private final Account account;
 488		private String fingerprint = null;
 489
 490		public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) {
 491			this(account, store, remoteAddress);
 492			this.fingerprint = fingerprint;
 493		}
 494
 495		public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
 496			this.cipher = new SessionCipher(store, remoteAddress);
 497			this.remoteAddress = remoteAddress;
 498			this.sqLiteAxolotlStore = store;
 499			this.account = account;
 500		}
 501
 502		public Integer getPreKeyId() {
 503			return preKeyId;
 504		}
 505
 506		public void resetPreKeyId() {
 507
 508			preKeyId = null;
 509		}
 510
 511		public String getFingerprint() {
 512			return fingerprint;
 513		}
 514
 515		public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) {
 516			byte[] plaintext = null;
 517			try {
 518				try {
 519					PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents());
 520					Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
 521					String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", "");
 522					if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) {
 523						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Had session with fingerprint "+ this.fingerprint+", received message with fingerprint "+fingerprint);
 524					} else {
 525						this.fingerprint = fingerprint;
 526						plaintext = cipher.decrypt(message);
 527						if (message.getPreKeyId().isPresent()) {
 528							preKeyId = message.getPreKeyId().get();
 529						}
 530					}
 531				} catch (InvalidMessageException | InvalidVersionException e) {
 532					Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received");
 533					WhisperMessage message = new WhisperMessage(incomingHeader.getContents());
 534					plaintext = cipher.decrypt(message);
 535				} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
 536					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
 537				}
 538			} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException  e) {
 539				Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
 540			}
 541			return plaintext;
 542		}
 543
 544		public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) {
 545			CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
 546			XmppAxolotlMessage.XmppAxolotlMessageHeader header =
 547					new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(),
 548							ciphertextMessage.serialize());
 549			return header;
 550		}
 551	}
 552
 553	private static class AxolotlAddressMap<T> {
 554		protected Map<String, Map<Integer, T>> map;
 555		protected final Object MAP_LOCK = new Object();
 556
 557		public AxolotlAddressMap() {
 558			this.map = new HashMap<>();
 559		}
 560
 561		public void put(AxolotlAddress address, T value) {
 562			synchronized (MAP_LOCK) {
 563				Map<Integer, T> devices = map.get(address.getName());
 564				if (devices == null) {
 565					devices = new HashMap<>();
 566					map.put(address.getName(), devices);
 567				}
 568				devices.put(address.getDeviceId(), value);
 569			}
 570		}
 571
 572		public T get(AxolotlAddress address) {
 573			synchronized (MAP_LOCK) {
 574				Map<Integer, T> devices = map.get(address.getName());
 575				if (devices == null) {
 576					return null;
 577				}
 578				return devices.get(address.getDeviceId());
 579			}
 580		}
 581
 582		public Map<Integer, T> getAll(AxolotlAddress address) {
 583			synchronized (MAP_LOCK) {
 584				Map<Integer, T> devices = map.get(address.getName());
 585				if (devices == null) {
 586					return new HashMap<>();
 587				}
 588				return devices;
 589			}
 590		}
 591
 592		public boolean hasAny(AxolotlAddress address) {
 593			synchronized (MAP_LOCK) {
 594				Map<Integer, T> devices = map.get(address.getName());
 595				return devices != null && !devices.isEmpty();
 596			}
 597		}
 598
 599		public void clear() {
 600			map.clear();
 601		}
 602
 603	}
 604
 605	private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
 606		private final XmppConnectionService xmppConnectionService;
 607		private final Account account;
 608
 609		public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
 610			super();
 611			this.xmppConnectionService = service;
 612			this.account = account;
 613			this.fillMap(store);
 614		}
 615
 616		private void fillMap(SQLiteAxolotlStore store) {
 617			for (Contact contact : account.getRoster().getContacts()) {
 618				Jid bareJid = contact.getJid().toBareJid();
 619				if (bareJid == null) {
 620					continue; // FIXME: handle this?
 621				}
 622				String address = bareJid.toString();
 623				List<Integer> deviceIDs = store.getSubDeviceSessions(address);
 624				for (Integer deviceId : deviceIDs) {
 625					AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId);
 626					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building session for remote address: "+axolotlAddress.toString());
 627					String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
 628					this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
 629				}
 630			}
 631		}
 632
 633		@Override
 634		public void put(AxolotlAddress address, XmppAxolotlSession value) {
 635			super.put(address, value);
 636			xmppConnectionService.syncRosterToDisk(account);
 637		}
 638	}
 639
 640	private static enum FetchStatus {
 641		PENDING,
 642		SUCCESS,
 643		ERROR
 644	}
 645
 646	private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
 647
 648	}
 649	
 650	public static String getLogprefix(Account account) {
 651		return LOGPREFIX+" ("+account.getJid().toBareJid().toString()+"): ";
 652	}
 653
 654	public AxolotlService(Account account, XmppConnectionService connectionService) {
 655		this.mXmppConnectionService = connectionService;
 656		this.account = account;
 657		this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
 658		this.deviceIds = new HashMap<>();
 659		this.messageCache = new HashMap<>();
 660		this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
 661		this.fetchStatusMap = new FetchStatusMap();
 662		this.executor = new SerialSingleThreadExecutor();
 663	}
 664
 665	public IdentityKey getOwnPublicKey() {
 666		return axolotlStore.getIdentityKeyPair().getPublicKey();
 667	}
 668
 669	public Set<IdentityKey> getPendingKeys() {
 670		return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString());
 671	}
 672
 673	public Set<IdentityKey> getPendingKeys(Contact contact) {
 674		return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString());
 675	}
 676
 677	private AxolotlAddress getAddressForJid(Jid jid) {
 678		return new AxolotlAddress(jid.toString(), 0);
 679	}
 680
 681	private Set<XmppAxolotlSession> findOwnSessions() {
 682		AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
 683		Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
 684		return ownDeviceSessions;
 685	}
 686
 687	private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
 688		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
 689		Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
 690		return sessions;
 691	}
 692
 693	private boolean hasAny(Contact contact) {
 694		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
 695		return sessions.hasAny(contactAddress);
 696	}
 697
 698	public void regenerateKeys() {
 699		axolotlStore.regenerate();
 700		sessions.clear();
 701		fetchStatusMap.clear();
 702		publishBundlesIfNeeded();
 703		publishOwnDeviceIdIfNeeded();
 704	}
 705
 706	public int getOwnDeviceId() {
 707		return axolotlStore.loadRegistrationId();
 708	}
 709
 710	public Set<Integer> getOwnDeviceIds() {
 711		return this.deviceIds.get(account.getJid().toBareJid());
 712	}
 713
 714	public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
 715		if(deviceIds.contains(getOwnDeviceId())) {
 716			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Skipping own Device ID:"+ jid + ":"+getOwnDeviceId());
 717			deviceIds.remove(getOwnDeviceId());
 718		}
 719		for(Integer i:deviceIds) {
 720			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding Device ID:"+ jid + ":"+i);
 721		}
 722		this.deviceIds.put(jid, deviceIds);
 723		publishOwnDeviceIdIfNeeded();
 724	}
 725
 726	public void wipeOtherPepDevices() {
 727		Set<Integer> deviceIds = new HashSet<>();
 728		deviceIds.add(getOwnDeviceId());
 729		IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
 730		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Wiping all other devices from Pep:" + publish);
 731		mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
 732			@Override
 733			public void onIqPacketReceived(Account account, IqPacket packet) {
 734				// TODO: implement this!
 735			}
 736		});
 737	}
 738
 739	public void publishOwnDeviceIdIfNeeded() {
 740		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
 741		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
 742			@Override
 743			public void onIqPacketReceived(Account account, IqPacket packet) {
 744				Element item = mXmppConnectionService.getIqParser().getItem(packet);
 745				Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
 746				if (deviceIds == null) {
 747					deviceIds = new HashSet<Integer>();
 748				}
 749				if (!deviceIds.contains(getOwnDeviceId())) {
 750					deviceIds.add(getOwnDeviceId());
 751					IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
 752					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish);
 753					mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
 754						@Override
 755						public void onIqPacketReceived(Account account, IqPacket packet) {
 756							// TODO: implement this!
 757						}
 758					});
 759				}
 760			}
 761		});
 762	}
 763
 764	public void publishBundlesIfNeeded() {
 765		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
 766		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
 767			@Override
 768			public void onIqPacketReceived(Account account, IqPacket packet) {
 769				PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
 770				Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
 771				boolean flush = false;
 772				if (bundle == null) {
 773					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid bundle:" + packet);
 774					bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null);
 775					flush = true;
 776				}
 777				if (keys == null) {
 778					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid prekeys:" + packet);
 779				}
 780				try {
 781					boolean changed = false;
 782					// Validate IdentityKey
 783					IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
 784					if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
 785						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
 786						changed = true;
 787					}
 788
 789					// Validate signedPreKeyRecord + ID
 790					SignedPreKeyRecord signedPreKeyRecord;
 791					int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
 792					try {
 793						signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
 794						if ( flush
 795								||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
 796								|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
 797							Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
 798							signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
 799							axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
 800							changed = true;
 801						}
 802					} catch (InvalidKeyIdException e) {
 803						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
 804						signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
 805						axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
 806						changed = true;
 807					}
 808
 809					// Validate PreKeys
 810					Set<PreKeyRecord> preKeyRecords = new HashSet<>();
 811					if (keys != null) {
 812						for (Integer id : keys.keySet()) {
 813							try {
 814								PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
 815								if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
 816									preKeyRecords.add(preKeyRecord);
 817								}
 818							} catch (InvalidKeyIdException ignored) {
 819							}
 820						}
 821					}
 822					int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
 823					if (newKeys > 0) {
 824						List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
 825								axolotlStore.getCurrentPreKeyId()+1, newKeys);
 826						preKeyRecords.addAll(newRecords);
 827						for (PreKeyRecord record : newRecords) {
 828							axolotlStore.storePreKey(record.getId(), record);
 829						}
 830						changed = true;
 831						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding " + newKeys + " new preKeys to PEP.");
 832					}
 833
 834
 835					if(changed) {
 836						IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
 837								signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
 838								preKeyRecords, getOwnDeviceId());
 839						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
 840						mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
 841							@Override
 842							public void onIqPacketReceived(Account account, IqPacket packet) {
 843								// TODO: implement this!
 844								Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Published bundle, got: " + packet);
 845							}
 846						});
 847					}
 848				} catch (InvalidKeyException e) {
 849						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
 850						return;
 851				}
 852			}
 853		});
 854	}
 855
 856	public boolean isContactAxolotlCapable(Contact contact) {
 857
 858		Jid jid = contact.getJid().toBareJid();
 859		AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
 860		return sessions.hasAny(address) ||
 861				( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
 862	}
 863	public SQLiteAxolotlStore.Trust getFingerprintTrust(String fingerprint) {
 864		return axolotlStore.getFingerprintTrust(fingerprint);
 865	}
 866
 867	public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) {
 868		axolotlStore.setFingerprintTrust(fingerprint, trust);
 869	}
 870
 871	private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address, final boolean flushWaitingQueueAfterFetch) {
 872		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
 873
 874		try {
 875			IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
 876					Jid.fromString(address.getName()), address.getDeviceId());
 877			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket);
 878			mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
 879				private void finish() {
 880					AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0);
 881					AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0);
 882					if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
 883							&& !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
 884						if (flushWaitingQueueAfterFetch) {
 885							conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL,
 886									new Conversation.OnMessageFound() {
 887										@Override
 888										public void onMessageFound(Message message) {
 889											processSending(message);
 890										}
 891									});
 892						}
 893						mXmppConnectionService.newKeysAvailable();
 894					}
 895				}
 896
 897				@Override
 898				public void onIqPacketReceived(Account account, IqPacket packet) {
 899					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing...");
 900					final IqParser parser = mXmppConnectionService.getIqParser();
 901					final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
 902					final PreKeyBundle bundle = parser.bundle(packet);
 903					if (preKeyBundleList.isEmpty() || bundle == null) {
 904						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet);
 905						fetchStatusMap.put(address, FetchStatus.ERROR);
 906						finish();
 907						return;
 908					}
 909					Random random = new Random();
 910					final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
 911					if (preKey == null) {
 912						//should never happen
 913						fetchStatusMap.put(address, FetchStatus.ERROR);
 914						finish();
 915						return;
 916					}
 917
 918					final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
 919							preKey.getPreKeyId(), preKey.getPreKey(),
 920							bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
 921							bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
 922
 923					axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
 924
 925					try {
 926						SessionBuilder builder = new SessionBuilder(axolotlStore, address);
 927						builder.process(preKeyBundle);
 928						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
 929						sessions.put(address, session);
 930						fetchStatusMap.put(address, FetchStatus.SUCCESS);
 931					} catch (UntrustedIdentityException|InvalidKeyException e) {
 932						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error building session for " + address + ": "
 933								+ e.getClass().getName() + ", " + e.getMessage());
 934						fetchStatusMap.put(address, FetchStatus.ERROR);
 935					}
 936
 937					finish();
 938				}
 939			});
 940		} catch (InvalidJidException e) {
 941			Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got address with invalid jid: " + address.getName());
 942		}
 943	}
 944
 945	public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
 946		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
 947		Jid contactJid = conversation.getContact().getJid().toBareJid();
 948		Set<AxolotlAddress> addresses = new HashSet<>();
 949		if(deviceIds.get(contactJid) != null) {
 950			for(Integer foreignId:this.deviceIds.get(contactJid)) {
 951				AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
 952				if(sessions.get(address) == null) {
 953					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
 954					if ( identityKey != null ) {
 955						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " +  address.toString() + ", adding to cache...");
 956						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
 957						sessions.put(address, session);
 958					} else {
 959						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
 960						addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
 961					}
 962				}
 963			}
 964		} else {
 965			Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
 966		}
 967		if(deviceIds.get(account.getJid().toBareJid()) != null) {
 968			for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) {
 969				AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
 970				if(sessions.get(address) == null) {
 971					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
 972					if ( identityKey != null ) {
 973						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " +  address.toString() + ", adding to cache...");
 974						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
 975						sessions.put(address, session);
 976					} else {
 977						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
 978						addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
 979					}
 980				}
 981			}
 982		}
 983
 984		return addresses;
 985	}
 986
 987	public boolean createSessionsIfNeeded(final Conversation conversation, final boolean flushWaitingQueueAfterFetch) {
 988		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
 989		boolean newSessions = false;
 990		Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
 991		for (AxolotlAddress address : addresses) {
 992			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
 993			FetchStatus status = fetchStatusMap.get(address);
 994			if ( status == null || status == FetchStatus.ERROR ) {
 995					fetchStatusMap.put(address, FetchStatus.PENDING);
 996					this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch);
 997					newSessions = true;
 998			} else {
 999				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already fetching bundle for " +  address.toString());
1000			}
1001		}
1002
1003		return newSessions;
1004	}
1005
1006	public boolean hasPendingKeyFetches(Conversation conversation) {
1007		AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(),0);
1008		AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0);
1009		return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
1010				||fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
1011
1012	}
1013
1014	@Nullable
1015	public XmppAxolotlMessage encrypt(Message message ){
1016		final String content;
1017		if (message.hasFileOnRemoteHost()) {
1018				content = message.getFileParams().url.toString();
1019			} else {
1020				content = message.getBody();
1021			}
1022		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
1023				getOwnDeviceId(), content);
1024
1025		if(findSessionsforContact(message.getContact()).isEmpty()) {
1026			return null;
1027		}
1028		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers...");
1029		for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
1030			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString());
1031			//if(!session.isTrusted()) {
1032			// TODO: handle this properly
1033			//              continue;
1034			//        }
1035			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
1036		}
1037		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers...");
1038		for (XmppAxolotlSession session : findOwnSessions()) {
1039			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString());
1040			//        if(!session.isTrusted()) {
1041			// TODO: handle this properly
1042			//          continue;
1043			//    }
1044			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
1045		}
1046
1047		return axolotlMessage;
1048	}
1049
1050	private void processSending(final Message message) {
1051		executor.execute(new Runnable() {
1052			@Override
1053			public void run() {
1054				MessagePacket packet = mXmppConnectionService.getMessageGenerator()
1055						.generateAxolotlChat(message);
1056				if (packet == null) {
1057					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
1058					//mXmppConnectionService.updateConversationUi();
1059				} else {
1060					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Generated message, caching: " + message.getUuid());
1061					messageCache.put(message.getUuid(), packet);
1062					mXmppConnectionService.resendMessage(message);
1063				}
1064			}
1065		});
1066	}
1067
1068	public void prepareMessage(final Message message) {
1069		if (!messageCache.containsKey(message.getUuid())) {
1070			boolean newSessions = createSessionsIfNeeded(message.getConversation(), true);
1071			if (!newSessions) {
1072				this.processSending(message);
1073			}
1074		}
1075	}
1076
1077	public MessagePacket fetchPacketFromCache(Message message) {
1078		MessagePacket packet = messageCache.get(message.getUuid());
1079		if (packet != null) {
1080			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache hit: " + message.getUuid());
1081			messageCache.remove(message.getUuid());
1082		} else {
1083			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache miss: " + message.getUuid());
1084		}
1085		return packet;
1086	}
1087
1088	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) {
1089		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
1090		AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
1091				message.getSenderDeviceId());
1092
1093		boolean newSession = false;
1094		XmppAxolotlSession session = sessions.get(senderAddress);
1095		if (session == null) {
1096			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message);
1097			// TODO: handle this properly
1098			IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey();
1099			if ( identityKey != null ) {
1100				session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", ""));
1101			} else {
1102				session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
1103			}
1104			newSession = true;
1105		}
1106
1107		for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
1108			if (header.getRecipientDeviceId() == getOwnDeviceId()) {
1109				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing...");
1110				byte[] payloadKey = session.processReceiving(header);
1111				if (payloadKey != null) {
1112					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message...");
1113					plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint());
1114				}
1115				Integer preKeyId = session.getPreKeyId();
1116				if (preKeyId != null) {
1117					publishBundlesIfNeeded();
1118					session.resetPreKeyId();
1119				}
1120				break;
1121			}
1122		}
1123
1124		if (newSession && plaintextMessage != null) {
1125			sessions.put(senderAddress, session);
1126		}
1127
1128		return plaintextMessage;
1129	}
1130}