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