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