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