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	public void publishBundlesIfNeeded() {
659		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId);
660		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
661			@Override
662			public void onIqPacketReceived(Account account, IqPacket packet) {
663				PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
664				Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
665				boolean flush = false;
666				if (bundle == null) {
667					Log.e(Config.LOGTAG, "Received invalid bundle:" + packet);
668					bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null);
669					flush = true;
670				}
671				if (keys == null) {
672					Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet);
673				}
674				try {
675					boolean changed = false;
676					// Validate IdentityKey
677					IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
678					if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
679						Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
680						changed = true;
681					}
682
683					// Validate signedPreKeyRecord + ID
684					SignedPreKeyRecord signedPreKeyRecord;
685					int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
686					try {
687						signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
688						if ( flush
689								||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
690								|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
691							Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
692							signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
693							axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
694							changed = true;
695						}
696					} catch (InvalidKeyIdException e) {
697						Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
698						signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
699						axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
700						changed = true;
701					}
702
703					// Validate PreKeys
704					Set<PreKeyRecord> preKeyRecords = new HashSet<>();
705					if (keys != null) {
706						for (Integer id : keys.keySet()) {
707							try {
708								PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
709								if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
710									preKeyRecords.add(preKeyRecord);
711								}
712							} catch (InvalidKeyIdException ignored) {
713							}
714						}
715					}
716					int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
717					if (newKeys > 0) {
718						List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
719								axolotlStore.getCurrentPreKeyId()+1, newKeys);
720						preKeyRecords.addAll(newRecords);
721						for (PreKeyRecord record : newRecords) {
722							axolotlStore.storePreKey(record.getId(), record);
723						}
724						changed = true;
725						Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP.");
726					}
727
728
729					if(changed) {
730						IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
731								signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
732								preKeyRecords, ownDeviceId);
733						Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
734						mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
735							@Override
736							public void onIqPacketReceived(Account account, IqPacket packet) {
737								// TODO: implement this!
738								Log.d(Config.LOGTAG, "Published bundle, got: " + packet);
739							}
740						});
741					}
742				} catch (InvalidKeyException e) {
743						Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
744						return;
745				}
746			}
747		});
748	}
749
750	public boolean isContactAxolotlCapable(Contact contact) {
751		Jid jid = contact.getJid().toBareJid();
752		AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
753		return sessions.hasAny(address) ||
754				( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
755	}
756
757	private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) {
758		Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId());
759
760		try {
761			IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
762					Jid.fromString(address.getName()), address.getDeviceId());
763			Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket);
764			mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
765				@Override
766				public void onIqPacketReceived(Account account, IqPacket packet) {
767					Log.d(Config.LOGTAG, "Received preKey IQ packet, processing...");
768					final IqParser parser = mXmppConnectionService.getIqParser();
769					final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
770					final PreKeyBundle bundle = parser.bundle(packet);
771					if (preKeyBundleList.isEmpty() || bundle == null) {
772						Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet);
773						fetchStatusMap.put(address, FetchStatus.ERROR);
774						return;
775					}
776					Random random = new Random();
777					final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
778					if (preKey == null) {
779						//should never happen
780						fetchStatusMap.put(address, FetchStatus.ERROR);
781						return;
782					}
783
784					final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
785							preKey.getPreKeyId(), preKey.getPreKey(),
786							bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
787							bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
788
789					axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
790
791					try {
792						SessionBuilder builder = new SessionBuilder(axolotlStore, address);
793						builder.process(preKeyBundle);
794						XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address);
795						sessions.put(address, session);
796						fetchStatusMap.put(address, FetchStatus.SUCCESS);
797					} catch (UntrustedIdentityException|InvalidKeyException e) {
798						Log.d(Config.LOGTAG, "Error building session for " + address + ": "
799								+ e.getClass().getName() + ", " + e.getMessage());
800						fetchStatusMap.put(address, FetchStatus.ERROR);
801					}
802
803					AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0);
804					AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0);
805					if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
806							&& !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
807						conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL,
808								new Conversation.OnMessageFound() {
809									@Override
810									public void onMessageFound(Message message) {
811										processSending(message);
812									}
813								});
814					}
815				}
816			});
817		} catch (InvalidJidException e) {
818			Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName());
819		}
820	}
821
822	private boolean createSessionsIfNeeded(Conversation conversation) {
823		boolean newSessions = false;
824		Log.d(Config.LOGTAG, "Creating axolotl sessions if needed...");
825		Jid contactJid = conversation.getContact().getJid().toBareJid();
826		Set<AxolotlAddress> addresses = new HashSet<>();
827		if(deviceIds.get(contactJid) != null) {
828			for(Integer foreignId:this.deviceIds.get(contactJid)) {
829				Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId);
830				addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
831			}
832		} else {
833			Log.e(Config.LOGTAG, "Have no target devices in PEP!");
834		}
835		Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid());
836		if(deviceIds.get(account.getJid().toBareJid()) != null) {
837			for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) {
838				Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId);
839				addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
840			}
841		}
842		for (AxolotlAddress address : addresses) {
843			Log.d(Config.LOGTAG, "Processing device: " + address.toString());
844			FetchStatus status = fetchStatusMap.get(address);
845			XmppAxolotlSession session = sessions.get(address);
846			if ( session == null && ( status == null || status == FetchStatus.ERROR) ) {
847				fetchStatusMap.put(address, FetchStatus.PENDING);
848				this.buildSessionFromPEP(conversation,  address);
849				newSessions = true;
850			} else {
851				Log.d(Config.LOGTAG, "Already have session for " +  address.toString());
852			}
853		}
854		return newSessions;
855	}
856
857	@Nullable
858	public XmppAxolotlMessage encrypt(Message message ){
859		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
860				ownDeviceId, message.getBody());
861
862		if(findSessionsforContact(message.getContact()).isEmpty()) {
863			return null;
864		}
865		Log.d(Config.LOGTAG, "Building axolotl foreign headers...");
866		for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
867			Log.d(Config.LOGTAG, session.remoteAddress.toString());
868			//if(!session.isTrusted()) {
869			// TODO: handle this properly
870			//              continue;
871			//        }
872			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
873		}
874		Log.d(Config.LOGTAG, "Building axolotl own headers...");
875		for (XmppAxolotlSession session : findOwnSessions()) {
876			Log.d(Config.LOGTAG, session.remoteAddress.toString());
877			//        if(!session.isTrusted()) {
878			// TODO: handle this properly
879			//          continue;
880			//    }
881			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
882		}
883
884		return axolotlMessage;
885	}
886
887	private void processSending(final Message message) {
888		executor.execute(new Runnable() {
889			@Override
890			public void run() {
891				MessagePacket packet = mXmppConnectionService.getMessageGenerator()
892						.generateAxolotlChat(message);
893				if (packet == null) {
894					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
895				} else {
896					mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
897					mXmppConnectionService.sendMessagePacket(account, packet);
898				}
899			}
900		});
901	}
902
903	public void sendMessage(Message message) {
904		boolean newSessions = createSessionsIfNeeded(message.getConversation());
905
906		if (!newSessions) {
907			this.processSending(message);
908		}
909	}
910
911	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) {
912		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
913		AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
914				message.getSenderDeviceId());
915
916		XmppAxolotlSession session = sessions.get(senderAddress);
917		if (session == null) {
918			Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message);
919			// TODO: handle this properly
920			session = new XmppAxolotlSession(axolotlStore, senderAddress);
921			sessions.put(senderAddress,session);
922		}
923
924		for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
925			if (header.getRecipientDeviceId() == ownDeviceId) {
926				Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing...");
927				byte[] payloadKey = session.processReceiving(header);
928				if (payloadKey != null) {
929					Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message...");
930					plaintextMessage = message.decrypt(session, payloadKey);
931				}
932			}
933		}
934
935		return plaintextMessage;
936	}
937}