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