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