AxolotlService.java

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