AxolotlService.java

  1package eu.siacs.conversations.crypto.axolotl;
  2
  3import android.util.Base64;
  4import android.util.Log;
  5
  6import org.whispersystems.libaxolotl.AxolotlAddress;
  7import org.whispersystems.libaxolotl.DuplicateMessageException;
  8import org.whispersystems.libaxolotl.IdentityKey;
  9import org.whispersystems.libaxolotl.IdentityKeyPair;
 10import org.whispersystems.libaxolotl.InvalidKeyException;
 11import org.whispersystems.libaxolotl.InvalidKeyIdException;
 12import org.whispersystems.libaxolotl.InvalidMessageException;
 13import org.whispersystems.libaxolotl.InvalidVersionException;
 14import org.whispersystems.libaxolotl.LegacyMessageException;
 15import org.whispersystems.libaxolotl.NoSessionException;
 16import org.whispersystems.libaxolotl.SessionBuilder;
 17import org.whispersystems.libaxolotl.SessionCipher;
 18import org.whispersystems.libaxolotl.UntrustedIdentityException;
 19import org.whispersystems.libaxolotl.ecc.Curve;
 20import org.whispersystems.libaxolotl.ecc.ECKeyPair;
 21import org.whispersystems.libaxolotl.ecc.ECPublicKey;
 22import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
 23import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
 24import org.whispersystems.libaxolotl.protocol.WhisperMessage;
 25import org.whispersystems.libaxolotl.state.AxolotlStore;
 26import org.whispersystems.libaxolotl.state.PreKeyBundle;
 27import org.whispersystems.libaxolotl.state.PreKeyRecord;
 28import org.whispersystems.libaxolotl.state.SessionRecord;
 29import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
 30import org.whispersystems.libaxolotl.util.KeyHelper;
 31
 32import java.util.ArrayList;
 33import java.util.HashMap;
 34import java.util.HashSet;
 35import java.util.List;
 36import java.util.Map;
 37import java.util.Random;
 38import java.util.Set;
 39
 40import eu.siacs.conversations.Config;
 41import eu.siacs.conversations.entities.Account;
 42import eu.siacs.conversations.entities.Contact;
 43import eu.siacs.conversations.entities.Conversation;
 44import eu.siacs.conversations.parser.IqParser;
 45import eu.siacs.conversations.services.XmppConnectionService;
 46import eu.siacs.conversations.xml.Element;
 47import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 48import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 49import eu.siacs.conversations.xmpp.jid.Jid;
 50import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 51
 52public class AxolotlService {
 53
 54	public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
 55	public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
 56	public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys";
 57	public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle";
 58
 59	private final Account account;
 60	private final XmppConnectionService mXmppConnectionService;
 61	private final SQLiteAxolotlStore axolotlStore;
 62	private final SessionMap sessions;
 63	private final BundleMap bundleCache;
 64	private int ownDeviceId;
 65
 66	public static class SQLiteAxolotlStore implements AxolotlStore {
 67
 68		public static final String PREKEY_TABLENAME = "prekeys";
 69		public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
 70		public static final String SESSION_TABLENAME = "sessions";
 71		public static final String ACCOUNT = "account";
 72		public static final String DEVICE_ID = "device_id";
 73		public static final String ID = "id";
 74		public static final String KEY = "key";
 75		public static final String NAME = "name";
 76		public static final String TRUSTED = "trusted";
 77
 78		public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key";
 79		public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
 80		public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
 81
 82		private final Account account;
 83		private final XmppConnectionService mXmppConnectionService;
 84
 85		private final IdentityKeyPair identityKeyPair;
 86		private final int localRegistrationId;
 87		private int currentPreKeyId = 0;
 88
 89
 90		private static IdentityKeyPair generateIdentityKeyPair() {
 91			Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair...");
 92			ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
 93			IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
 94					identityKeyPairKeys.getPrivateKey());
 95			return ownKey;
 96		}
 97
 98		private static int generateRegistrationId() {
 99			Log.d(Config.LOGTAG, "Generating axolotl registration ID...");
100			int reg_id = KeyHelper.generateRegistrationId(false);
101			return reg_id;
102		}
103
104		public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
105			this.account = account;
106			this.mXmppConnectionService = service;
107			this.identityKeyPair = loadIdentityKeyPair();
108			this.localRegistrationId = loadRegistrationId();
109			this.currentPreKeyId = loadCurrentPreKeyId();
110			for( SignedPreKeyRecord record:loadSignedPreKeys()) {
111				Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId());
112			}
113		}
114
115		public int getCurrentPreKeyId() {
116			return currentPreKeyId;
117		}
118
119		// --------------------------------------
120		// IdentityKeyStore
121		// --------------------------------------
122
123		private IdentityKeyPair loadIdentityKeyPair() {
124			String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR);
125			IdentityKeyPair ownKey;
126			if( serializedKey != null ) {
127				try {
128					ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT));
129					return ownKey;
130				} catch (InvalidKeyException e) {
131					Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage());
132//                    return null;
133				}
134			} //else {
135				Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid());
136				ownKey = generateIdentityKeyPair();
137				boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT));
138				if(success) {
139					mXmppConnectionService.databaseBackend.updateAccount(account);
140				} else {
141					Log.e(Config.LOGTAG, "Failed to write new key to the database!");
142				}
143			//}
144			return ownKey;
145		}
146
147		private int loadRegistrationId() {
148			String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
149			int reg_id;
150			if (regIdString != null) {
151				reg_id = Integer.valueOf(regIdString);
152			} else {
153				Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid());
154				reg_id = generateRegistrationId();
155				boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id);
156				if(success) {
157					mXmppConnectionService.databaseBackend.updateAccount(account);
158				} else {
159					Log.e(Config.LOGTAG, "Failed to write new key to the database!");
160				}
161			}
162			return reg_id;
163		}
164
165		private int loadCurrentPreKeyId() {
166			String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
167			int reg_id;
168			if (regIdString != null) {
169				reg_id = Integer.valueOf(regIdString);
170			} else {
171				Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid());
172				reg_id = 0;
173			}
174			return reg_id;
175		}
176
177
178		/**
179		 * Get the local client's identity key pair.
180		 *
181		 * @return The local client's persistent identity key pair.
182		 */
183		@Override
184		public IdentityKeyPair getIdentityKeyPair() {
185			return identityKeyPair;
186		}
187
188		/**
189		 * Return the local client's registration ID.
190		 * <p/>
191		 * Clients should maintain a registration ID, a random number
192		 * between 1 and 16380 that's generated once at install time.
193		 *
194		 * @return the local client's registration ID.
195		 */
196		@Override
197		public int getLocalRegistrationId() {
198			return localRegistrationId;
199		}
200
201		/**
202		 * Save a remote client's identity key
203		 * <p/>
204		 * Store a remote client's identity key as trusted.
205		 *
206		 * @param name        The name of the remote client.
207		 * @param identityKey The remote client's identity key.
208		 */
209		@Override
210		public void saveIdentity(String name, IdentityKey identityKey) {
211			try {
212				Jid contactJid = Jid.fromString(name);
213				Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
214				if (conversation != null) {
215					conversation.getContact().addAxolotlIdentityKey(identityKey);
216					mXmppConnectionService.updateConversationUi();
217					mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
218				}
219			} catch (final InvalidJidException e) {
220				Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString());
221			}
222		}
223
224		/**
225		 * Verify a remote client's identity key.
226		 * <p/>
227		 * Determine whether a remote client's identity is trusted.  Convention is
228		 * that the TextSecure protocol is 'trust on first use.'  This means that
229		 * an identity key is considered 'trusted' if there is no entry for the recipient
230		 * in the local store, or if it matches the saved key for a recipient in the local
231		 * store.  Only if it mismatches an entry in the local store is it considered
232		 * 'untrusted.'
233		 *
234		 * @param name        The name of the remote client.
235		 * @param identityKey The identity key to verify.
236		 * @return true if trusted, false if untrusted.
237		 */
238		@Override
239		public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
240			try {
241				Jid contactJid = Jid.fromString(name);
242				Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
243				if (conversation != null) {
244					List<IdentityKey> trustedKeys = conversation.getContact().getAxolotlIdentityKeys();
245					return trustedKeys.isEmpty() || trustedKeys.contains(identityKey);
246				} else {
247					return false;
248				}
249			} catch (final InvalidJidException e) {
250				Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString());
251				return false;
252			}
253		}
254
255		// --------------------------------------
256		// SessionStore
257		// --------------------------------------
258
259		/**
260		 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
261		 * or a new SessionRecord if one does not currently exist.
262		 * <p/>
263		 * It is important that implementations return a copy of the current durable information.  The
264		 * returned SessionRecord may be modified, but those changes should not have an effect on the
265		 * durable session state (what is returned by subsequent calls to this method) without the
266		 * store method being called here first.
267		 *
268		 * @param address The name and device ID of the remote client.
269		 * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
270		 * a new SessionRecord if one does not currently exist.
271		 */
272		@Override
273		public SessionRecord loadSession(AxolotlAddress address) {
274			SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
275			return (session!=null)?session:new SessionRecord();
276		}
277
278		/**
279		 * Returns all known devices with active sessions for a recipient
280		 *
281		 * @param name the name of the client.
282		 * @return all known sub-devices with active sessions.
283		 */
284		@Override
285		public List<Integer> getSubDeviceSessions(String name) {
286			return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
287					new AxolotlAddress(name,0));
288		}
289
290		/**
291		 * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
292		 *
293		 * @param address the address of the remote client.
294		 * @param record  the current SessionRecord for the remote client.
295		 */
296		@Override
297		public void storeSession(AxolotlAddress address, SessionRecord record) {
298			mXmppConnectionService.databaseBackend.storeSession(account, address, record);
299		}
300
301		/**
302		 * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
303		 *
304		 * @param address the address of the remote client.
305		 * @return true if a {@link SessionRecord} exists, false otherwise.
306		 */
307		@Override
308		public boolean containsSession(AxolotlAddress address) {
309			return mXmppConnectionService.databaseBackend.containsSession(account, address);
310		}
311
312		/**
313		 * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
314		 *
315		 * @param address the address of the remote client.
316		 */
317		@Override
318		public void deleteSession(AxolotlAddress address) {
319			mXmppConnectionService.databaseBackend.deleteSession(account, address);
320		}
321
322		/**
323		 * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
324		 *
325		 * @param name the name of the remote client.
326		 */
327		@Override
328		public void deleteAllSessions(String name) {
329			mXmppConnectionService.databaseBackend.deleteAllSessions(account,
330					new AxolotlAddress(name, 0));
331		}
332
333		public boolean isTrustedSession(AxolotlAddress address) {
334			return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address);
335		}
336
337		public void setTrustedSession(AxolotlAddress address, boolean trusted) {
338			mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted);
339		}
340
341		// --------------------------------------
342		// PreKeyStore
343		// --------------------------------------
344
345		/**
346		 * Load a local PreKeyRecord.
347		 *
348		 * @param preKeyId the ID of the local PreKeyRecord.
349		 * @return the corresponding PreKeyRecord.
350		 * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
351		 */
352		@Override
353		public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
354			PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
355			if(record == null) {
356				throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
357			}
358			return record;
359		}
360
361		/**
362		 * Store a local PreKeyRecord.
363		 *
364		 * @param preKeyId the ID of the PreKeyRecord to store.
365		 * @param record   the PreKeyRecord.
366		 */
367		@Override
368		public void storePreKey(int preKeyId, PreKeyRecord record) {
369			mXmppConnectionService.databaseBackend.storePreKey(account, record);
370			currentPreKeyId = preKeyId;
371			boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId));
372			if(success) {
373				mXmppConnectionService.databaseBackend.updateAccount(account);
374			} else {
375				Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!");
376			}
377		}
378
379		/**
380		 * @param preKeyId A PreKeyRecord ID.
381		 * @return true if the store has a record for the preKeyId, otherwise false.
382		 */
383		@Override
384		public boolean containsPreKey(int preKeyId) {
385			return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
386		}
387
388		/**
389		 * Delete a PreKeyRecord from local storage.
390		 *
391		 * @param preKeyId The ID of the PreKeyRecord to remove.
392		 */
393		@Override
394		public void removePreKey(int preKeyId) {
395			mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
396		}
397
398		// --------------------------------------
399		// SignedPreKeyStore
400		// --------------------------------------
401
402		/**
403		 * Load a local SignedPreKeyRecord.
404		 *
405		 * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
406		 * @return the corresponding SignedPreKeyRecord.
407		 * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
408		 */
409		@Override
410		public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
411			SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
412			if(record == null) {
413				throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
414			}
415			return record;
416		}
417
418		/**
419		 * Load all local SignedPreKeyRecords.
420		 *
421		 * @return All stored SignedPreKeyRecords.
422		 */
423		@Override
424		public List<SignedPreKeyRecord> loadSignedPreKeys() {
425			return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
426		}
427
428		/**
429		 * Store a local SignedPreKeyRecord.
430		 *
431		 * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
432		 * @param record         the SignedPreKeyRecord.
433		 */
434		@Override
435		public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
436			mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
437		}
438
439		/**
440		 * @param signedPreKeyId A SignedPreKeyRecord ID.
441		 * @return true if the store has a record for the signedPreKeyId, otherwise false.
442		 */
443		@Override
444		public boolean containsSignedPreKey(int signedPreKeyId) {
445			return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
446		}
447
448		/**
449		 * Delete a SignedPreKeyRecord from local storage.
450		 *
451		 * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
452		 */
453		@Override
454		public void removeSignedPreKey(int signedPreKeyId) {
455			mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
456		}
457	}
458
459	public static class XmppAxolotlSession {
460		private SessionCipher cipher;
461		private boolean isTrusted = false;
462		private SQLiteAxolotlStore sqLiteAxolotlStore;
463		private AxolotlAddress remoteAddress;
464
465		public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
466			this.cipher = new SessionCipher(store, remoteAddress);
467			this.remoteAddress = remoteAddress;
468			this.sqLiteAxolotlStore = store;
469			this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress);
470		}
471
472		public void trust() {
473			sqLiteAxolotlStore.setTrustedSession(remoteAddress, true);
474			this.isTrusted = true;
475		}
476
477		public boolean isTrusted() {
478			return this.isTrusted;
479		}
480
481		public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) {
482			byte[] plaintext = null;
483			try {
484				try {
485					PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents());
486					Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
487					plaintext = cipher.decrypt(message);
488				} catch (InvalidMessageException|InvalidVersionException e) {
489					WhisperMessage message = new WhisperMessage(incomingHeader.getContents());
490					plaintext = cipher.decrypt(message);
491				} catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) {
492					Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage());
493				}
494			} catch (LegacyMessageException|InvalidMessageException e) {
495				Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage());
496			} catch (DuplicateMessageException|NoSessionException e) {
497				Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage());
498			}
499			return plaintext;
500		}
501
502		public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) {
503			CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
504			XmppAxolotlMessage.XmppAxolotlMessageHeader header =
505					new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(),
506							ciphertextMessage.serialize());
507			return header;
508		}
509	}
510
511	private static class AxolotlAddressMap<T> {
512		protected Map<String, Map<Integer,T>> map;
513		protected final Object MAP_LOCK = new Object();
514
515		public AxolotlAddressMap() {
516			this.map = new HashMap<>();
517		}
518
519		public void put(AxolotlAddress address, T value) {
520			synchronized (MAP_LOCK) {
521				Map<Integer, T> devices = map.get(address.getName());
522				if (devices == null) {
523					devices = new HashMap<>();
524					map.put(address.getName(), devices);
525				}
526				devices.put(address.getDeviceId(), value);
527			}
528		}
529
530		public T get(AxolotlAddress address) {
531			synchronized (MAP_LOCK) {
532				Map<Integer, T> devices = map.get(address.getName());
533				if(devices == null) {
534					return null;
535				}
536				return devices.get(address.getDeviceId());
537			}
538		}
539
540		public Map<Integer, T> getAll(AxolotlAddress address) {
541			synchronized (MAP_LOCK) {
542				Map<Integer, T> devices = map.get(address.getName());
543				if(devices == null) {
544					return new HashMap<>();
545				}
546				return devices;
547			}
548		}
549
550		public boolean hasAny(AxolotlAddress address) {
551			synchronized (MAP_LOCK) {
552				Map<Integer, T> devices = map.get(address.getName());
553				return devices != null && !devices.isEmpty();
554			}
555		}
556
557
558	}
559
560	private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
561
562		public SessionMap(SQLiteAxolotlStore store, Account account) {
563			super();
564			this.fillMap(store, account);
565		}
566
567		private void fillMap(SQLiteAxolotlStore store, Account account) {
568			for(Contact contact:account.getRoster().getContacts()){
569				Jid bareJid = contact.getJid().toBareJid();
570				if(bareJid == null) {
571					continue; // FIXME: handle this?
572				}
573				String address = bareJid.toString();
574				List<Integer> deviceIDs = store.getSubDeviceSessions(address);
575				for(Integer deviceId:deviceIDs) {
576					AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId);
577					this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress));
578				}
579			}
580		}
581
582	}
583
584	private static class BundleMap extends AxolotlAddressMap<PreKeyBundle> {
585
586	}
587
588	public AxolotlService(Account account, XmppConnectionService connectionService) {
589		this.mXmppConnectionService = connectionService;
590		this.account = account;
591		this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
592		this.sessions = new SessionMap(axolotlStore, account);
593		this.bundleCache = new BundleMap();
594		this.ownDeviceId = axolotlStore.getLocalRegistrationId();
595	}
596
597	public void trustSession(AxolotlAddress counterpart) {
598		XmppAxolotlSession session = sessions.get(counterpart);
599		if(session != null) {
600			session.trust();
601		}
602	}
603
604	public boolean isTrustedSession(AxolotlAddress counterpart) {
605		XmppAxolotlSession session = sessions.get(counterpart);
606		return session != null && session.isTrusted();
607	}
608
609	private AxolotlAddress getAddressForJid(Jid jid) {
610		return new AxolotlAddress(jid.toString(), 0);
611	}
612
613	private Set<XmppAxolotlSession> findOwnSessions() {
614		AxolotlAddress ownAddress = getAddressForJid(account.getJid());
615		Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
616		return ownDeviceSessions;
617	}
618
619	private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
620		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
621		Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
622		return sessions;
623	}
624
625	private boolean hasAny(Contact contact) {
626		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
627		return sessions.hasAny(contactAddress);
628	}
629
630	public int getOwnDeviceId() {
631		return ownDeviceId;
632	}
633
634	public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) {
635		final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId);
636		if (sessions.get(address) != null) {
637			return;
638		}
639
640		synchronized (bundleCache) {
641			PreKeyBundle bundle = bundleCache.get(address);
642			if (bundle == null) {
643				bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null);
644				bundleCache.put(address, bundle);
645			}
646
647			if(bundle.getPreKey() == null) {
648				Log.d(Config.LOGTAG, "No preKey in cache, fetching...");
649				IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId);
650				mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() {
651					@Override
652					public void onIqPacketReceived(Account account, IqPacket packet) {
653						synchronized (bundleCache) {
654							Log.d(Config.LOGTAG, "Received preKey IQ packet, processing...");
655							final IqParser parser = mXmppConnectionService.getIqParser();
656							final PreKeyBundle bundle = bundleCache.get(address);
657							final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
658							if (preKeyBundleList.isEmpty()) {
659								Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet);
660								return;
661							}
662							Random random = new Random();
663							final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
664							if (bundle == null || newBundle == null) {
665								//should never happen
666								return;
667							}
668
669							final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
670									bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(),
671									bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
672									bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
673
674							bundleCache.put(address, mergedBundle);
675						}
676					}
677				});
678			}
679			if(bundle.getIdentityKey() == null) {
680				Log.d(Config.LOGTAG, "No bundle in cache, fetching...");
681				IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId);
682				mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() {
683					@Override
684					public void onIqPacketReceived(Account account, IqPacket packet) {
685						synchronized (bundleCache) {
686							Log.d(Config.LOGTAG, "Received bundle IQ packet, processing...");
687							final IqParser parser = mXmppConnectionService.getIqParser();
688							final PreKeyBundle bundle = bundleCache.get(address);
689							final PreKeyBundle newBundle = parser.bundle(packet);
690							if( bundle == null || newBundle == null ) {
691								Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet);
692								//should never happen
693								return;
694							}
695
696							final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
697									bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(),
698									newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(),
699									newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey());
700
701							axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey());
702							bundleCache.put(address, mergedBundle);
703						}
704					}
705				});
706			}
707		}
708	}
709
710	public void publishOwnDeviceIdIfNeeded() {
711		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
712		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
713			@Override
714			public void onIqPacketReceived(Account account, IqPacket packet) {
715				Element item = mXmppConnectionService.getIqParser().getItem(packet);
716				List<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
717				if(deviceIds == null) {
718					deviceIds = new ArrayList<>();
719				}
720				if(!deviceIds.contains(getOwnDeviceId())) {
721					Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing...");
722					deviceIds.add(getOwnDeviceId());
723					IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
724					mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
725						@Override
726						public void onIqPacketReceived(Account account, IqPacket packet) {
727							// TODO: implement this!
728						}
729					});
730				}
731			}
732		});
733	}
734
735	public void publishBundleIfNeeded() {
736		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId);
737		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
738			@Override
739			public void onIqPacketReceived(Account account, IqPacket packet) {
740				PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
741				if(bundle == null) {
742					Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing...");
743					int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
744					try {
745						SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey(
746								axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1);
747						axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
748						IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle(
749								signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
750								ownDeviceId);
751						mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
752							@Override
753							public void onIqPacketReceived(Account account, IqPacket packet) {
754								// TODO: implement this!
755								Log.d(Config.LOGTAG, "Published bundle, got: " + packet);
756							}
757						});
758					} catch (InvalidKeyException e) {
759						Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
760					}
761				}
762			}
763		});
764	}
765
766	public void publishPreKeysIfNeeded() {
767		IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId);
768		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
769			@Override
770			public void onIqPacketReceived(Account account, IqPacket packet) {
771				Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
772				if(keys == null || keys.isEmpty()) {
773					Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing...");
774					List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(
775							axolotlStore.getCurrentPreKeyId(), 100);
776					for(PreKeyRecord record : preKeyRecords) {
777						axolotlStore.storePreKey(record.getId(), record);
778					}
779					IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys(
780							preKeyRecords, ownDeviceId);
781
782					mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
783						@Override
784						public void onIqPacketReceived(Account account, IqPacket packet) {
785							Log.d(Config.LOGTAG, "Published prekeys, got: " + packet);
786							// TODO: implement this!
787						}
788					});
789				}
790			}
791		});
792	}
793
794
795	public boolean isContactAxolotlCapable(Contact contact) {
796		AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
797		return sessions.hasAny(address) || bundleCache.hasAny(address);
798	}
799
800	public void initiateSynchronousSession(Contact contact) {
801
802	}
803
804	private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException {
805		Log.d(Config.LOGTAG, "Creating axolotl sessions if needed...");
806		AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
807		for(Integer deviceId: bundleCache.getAll(address).keySet()) {
808			Log.d(Config.LOGTAG, "Processing device ID: " + deviceId);
809			AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId);
810			if(sessions.get(remoteAddress) == null) {
811				Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId);
812				SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress);
813				try {
814					builder.process(bundleCache.get(remoteAddress));
815					XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress);
816					sessions.put(remoteAddress, session);
817				} catch (InvalidKeyException e) {
818					Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage());
819				} catch (UntrustedIdentityException e) {
820					Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage());
821				}
822			} else {
823				Log.d(Config.LOGTAG, "Already have session for " + deviceId);
824			}
825		}
826		if(!this.hasAny(contact)) {
827			Log.e(Config.LOGTAG, "No Axolotl sessions available!");
828			throw new NoSessionsCreatedException(); // FIXME: proper error handling
829		}
830	}
831
832	public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException {
833		XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage);
834		createSessionsIfNeeded(contact);
835		Log.d(Config.LOGTAG, "Building axolotl foreign headers...");
836
837		for(XmppAxolotlSession session : findSessionsforContact(contact)) {
838//            if(!session.isTrusted()) {
839				// TODO: handle this properly
840  //              continue;
841	//        }
842			message.addHeader(session.processSending(message.getInnerKey()));
843		}
844		Log.d(Config.LOGTAG, "Building axolotl own headers...");
845		for(XmppAxolotlSession session : findOwnSessions()) {
846	//        if(!session.isTrusted()) {
847				// TODO: handle this properly
848	  //          continue;
849		//    }
850			message.addHeader(session.processSending(message.getInnerKey()));
851		}
852
853		return message;
854	}
855
856	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) {
857		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
858		AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(),
859				message.getSenderDeviceId());
860
861		XmppAxolotlSession session = sessions.get(senderAddress);
862		if (session == null) {
863			Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message);
864			// TODO: handle this properly
865			session = new XmppAxolotlSession(axolotlStore, senderAddress);
866
867		}
868
869		for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
870			if (header.getRecipientDeviceId() == ownDeviceId) {
871				Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing...");
872				byte[] payloadKey = session.processReceiving(header);
873				if (payloadKey != null) {
874					Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message...");
875					plaintextMessage = message.decrypt(session, payloadKey);
876				}
877			}
878		}
879
880		return plaintextMessage;
881	}
882}