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