AxolotlService.java

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