AxolotlService.java

  1package eu.siacs.conversations.crypto.axolotl;
  2
  3import android.support.annotation.NonNull;
  4import android.support.annotation.Nullable;
  5import android.util.Log;
  6
  7import org.bouncycastle.jce.provider.BouncyCastleProvider;
  8import org.whispersystems.libaxolotl.AxolotlAddress;
  9import org.whispersystems.libaxolotl.IdentityKey;
 10import org.whispersystems.libaxolotl.IdentityKeyPair;
 11import org.whispersystems.libaxolotl.InvalidKeyException;
 12import org.whispersystems.libaxolotl.InvalidKeyIdException;
 13import org.whispersystems.libaxolotl.SessionBuilder;
 14import org.whispersystems.libaxolotl.UntrustedIdentityException;
 15import org.whispersystems.libaxolotl.ecc.ECPublicKey;
 16import org.whispersystems.libaxolotl.state.PreKeyBundle;
 17import org.whispersystems.libaxolotl.state.PreKeyRecord;
 18import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
 19import org.whispersystems.libaxolotl.util.KeyHelper;
 20
 21import java.security.Security;
 22import java.util.Arrays;
 23import java.util.HashMap;
 24import java.util.HashSet;
 25import java.util.List;
 26import java.util.Map;
 27import java.util.Random;
 28import java.util.Set;
 29
 30import eu.siacs.conversations.Config;
 31import eu.siacs.conversations.entities.Account;
 32import eu.siacs.conversations.entities.Contact;
 33import eu.siacs.conversations.entities.Conversation;
 34import eu.siacs.conversations.entities.Message;
 35import eu.siacs.conversations.parser.IqParser;
 36import eu.siacs.conversations.services.XmppConnectionService;
 37import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 38import eu.siacs.conversations.xml.Element;
 39import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 40import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 41import eu.siacs.conversations.xmpp.jid.Jid;
 42import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 43
 44public class AxolotlService {
 45
 46	public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
 47	public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
 48	public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
 49
 50	public static final String LOGPREFIX = "AxolotlService";
 51
 52	public static final int NUM_KEYS_TO_PUBLISH = 100;
 53
 54	private final Account account;
 55	private final XmppConnectionService mXmppConnectionService;
 56	private final SQLiteAxolotlStore axolotlStore;
 57	private final SessionMap sessions;
 58	private final Map<Jid, Set<Integer>> deviceIds;
 59	private final Map<String, XmppAxolotlMessage> messageCache;
 60	private final FetchStatusMap fetchStatusMap;
 61	private final SerialSingleThreadExecutor executor;
 62
 63	private static class AxolotlAddressMap<T> {
 64		protected Map<String, Map<Integer, T>> map;
 65		protected final Object MAP_LOCK = new Object();
 66
 67		public AxolotlAddressMap() {
 68			this.map = new HashMap<>();
 69		}
 70
 71		public void put(AxolotlAddress address, T value) {
 72			synchronized (MAP_LOCK) {
 73				Map<Integer, T> devices = map.get(address.getName());
 74				if (devices == null) {
 75					devices = new HashMap<>();
 76					map.put(address.getName(), devices);
 77				}
 78				devices.put(address.getDeviceId(), value);
 79			}
 80		}
 81
 82		public T get(AxolotlAddress address) {
 83			synchronized (MAP_LOCK) {
 84				Map<Integer, T> devices = map.get(address.getName());
 85				if (devices == null) {
 86					return null;
 87				}
 88				return devices.get(address.getDeviceId());
 89			}
 90		}
 91
 92		public Map<Integer, T> getAll(AxolotlAddress address) {
 93			synchronized (MAP_LOCK) {
 94				Map<Integer, T> devices = map.get(address.getName());
 95				if (devices == null) {
 96					return new HashMap<>();
 97				}
 98				return devices;
 99			}
100		}
101
102		public boolean hasAny(AxolotlAddress address) {
103			synchronized (MAP_LOCK) {
104				Map<Integer, T> devices = map.get(address.getName());
105				return devices != null && !devices.isEmpty();
106			}
107		}
108
109		public void clear() {
110			map.clear();
111		}
112
113	}
114
115	private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
116		private final XmppConnectionService xmppConnectionService;
117		private final Account account;
118
119		public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
120			super();
121			this.xmppConnectionService = service;
122			this.account = account;
123			this.fillMap(store);
124		}
125
126		private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
127			for (Integer deviceId : deviceIds) {
128				AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
129				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
130				String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
131				this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
132			}
133		}
134
135		private void fillMap(SQLiteAxolotlStore store) {
136			List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
137			putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
138			for (Contact contact : account.getRoster().getContacts()) {
139				Jid bareJid = contact.getJid().toBareJid();
140				if (bareJid == null) {
141					continue; // FIXME: handle this?
142				}
143				String address = bareJid.toString();
144				deviceIds = store.getSubDeviceSessions(address);
145				putDevicesForJid(address, deviceIds, store);
146			}
147
148		}
149
150		@Override
151		public void put(AxolotlAddress address, XmppAxolotlSession value) {
152			super.put(address, value);
153			value.setNotFresh();
154			xmppConnectionService.syncRosterToDisk(account);
155		}
156
157		public void put(XmppAxolotlSession session) {
158			this.put(session.getRemoteAddress(), session);
159		}
160	}
161
162	private static enum FetchStatus {
163		PENDING,
164		SUCCESS,
165		ERROR
166	}
167
168	private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
169
170	}
171
172	public static String getLogprefix(Account account) {
173		return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
174	}
175
176	public AxolotlService(Account account, XmppConnectionService connectionService) {
177		if (Security.getProvider("BC") == null) {
178			Security.addProvider(new BouncyCastleProvider());
179		}
180		this.mXmppConnectionService = connectionService;
181		this.account = account;
182		this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
183		this.deviceIds = new HashMap<>();
184		this.messageCache = new HashMap<>();
185		this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
186		this.fetchStatusMap = new FetchStatusMap();
187		this.executor = new SerialSingleThreadExecutor();
188	}
189
190	public IdentityKey getOwnPublicKey() {
191		return axolotlStore.getIdentityKeyPair().getPublicKey();
192	}
193
194	public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
195		return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
196	}
197
198	public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
199		return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
200	}
201
202	public long getNumTrustedKeys(Contact contact) {
203		return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
204	}
205
206	private AxolotlAddress getAddressForJid(Jid jid) {
207		return new AxolotlAddress(jid.toString(), 0);
208	}
209
210	private Set<XmppAxolotlSession> findOwnSessions() {
211		AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
212		Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
213		return ownDeviceSessions;
214	}
215
216	private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
217		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
218		Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
219		return sessions;
220	}
221
222	private boolean hasAny(Contact contact) {
223		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
224		return sessions.hasAny(contactAddress);
225	}
226
227	public void regenerateKeys() {
228		axolotlStore.regenerate();
229		sessions.clear();
230		fetchStatusMap.clear();
231		publishBundlesIfNeeded();
232		publishOwnDeviceIdIfNeeded();
233	}
234
235	public int getOwnDeviceId() {
236		return axolotlStore.getLocalRegistrationId();
237	}
238
239	public Set<Integer> getOwnDeviceIds() {
240		return this.deviceIds.get(account.getJid().toBareJid());
241	}
242
243	private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
244	                                final XmppAxolotlSession.Trust from,
245	                                final XmppAxolotlSession.Trust to) {
246		for (Integer deviceId : deviceIds) {
247			AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
248			XmppAxolotlSession session = sessions.get(address);
249			if (session != null && session.getFingerprint() != null
250					&& session.getTrust() == from) {
251				session.setTrust(to);
252			}
253		}
254	}
255
256	public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
257		if (jid.toBareJid().equals(account.getJid().toBareJid())) {
258			if (deviceIds.contains(getOwnDeviceId())) {
259				deviceIds.remove(getOwnDeviceId());
260			}
261			for (Integer deviceId : deviceIds) {
262				AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
263				if (sessions.get(ownDeviceAddress) == null) {
264					buildSessionFromPEP(ownDeviceAddress);
265				}
266			}
267		}
268		Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
269		expiredDevices.removeAll(deviceIds);
270		setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
271				XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
272		setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
273				XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
274		setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
275				XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
276		Set<Integer> newDevices = new HashSet<>(deviceIds);
277		setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
278				XmppAxolotlSession.Trust.TRUSTED);
279		setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
280				XmppAxolotlSession.Trust.UNDECIDED);
281		setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
282				XmppAxolotlSession.Trust.UNTRUSTED);
283		this.deviceIds.put(jid, deviceIds);
284		mXmppConnectionService.keyStatusUpdated();
285		publishOwnDeviceIdIfNeeded();
286	}
287
288	public void wipeOtherPepDevices() {
289		Set<Integer> deviceIds = new HashSet<>();
290		deviceIds.add(getOwnDeviceId());
291		IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
292		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
293		mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
294			@Override
295			public void onIqPacketReceived(Account account, IqPacket packet) {
296				// TODO: implement this!
297			}
298		});
299	}
300
301	public void purgeKey(IdentityKey identityKey) {
302		axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
303	}
304
305	public void publishOwnDeviceIdIfNeeded() {
306		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
307		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
308			@Override
309			public void onIqPacketReceived(Account account, IqPacket packet) {
310				if (packet.getType() == IqPacket.TYPE.RESULT) {
311					Element item = mXmppConnectionService.getIqParser().getItem(packet);
312					Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
313					if (deviceIds == null) {
314						deviceIds = new HashSet<Integer>();
315					}
316					if (!deviceIds.contains(getOwnDeviceId())) {
317						deviceIds.add(getOwnDeviceId());
318						IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
319						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish);
320						mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
321							@Override
322							public void onIqPacketReceived(Account account, IqPacket packet) {
323								// TODO: implement this!
324							}
325						});
326					}
327				} else {
328					Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing device ID:" + packet.findChild("error"));
329				}
330			}
331		});
332	}
333
334	public void publishBundlesIfNeeded() {
335		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
336		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
337			@Override
338			public void onIqPacketReceived(Account account, IqPacket packet) {
339				if (packet.getType() == IqPacket.TYPE.RESULT) {
340					PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
341					Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
342					boolean flush = false;
343					if (bundle == null) {
344						Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
345						bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
346						flush = true;
347					}
348					if (keys == null) {
349						Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
350					}
351					try {
352						boolean changed = false;
353						// Validate IdentityKey
354						IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
355						if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
356							Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
357							changed = true;
358						}
359
360						// Validate signedPreKeyRecord + ID
361						SignedPreKeyRecord signedPreKeyRecord;
362						int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
363						try {
364							signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
365							if (flush
366									|| !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
367									|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
368								Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
369								signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
370								axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
371								changed = true;
372							}
373						} catch (InvalidKeyIdException e) {
374							Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
375							signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
376							axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
377							changed = true;
378						}
379
380						// Validate PreKeys
381						Set<PreKeyRecord> preKeyRecords = new HashSet<>();
382						if (keys != null) {
383							for (Integer id : keys.keySet()) {
384								try {
385									PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
386									if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
387										preKeyRecords.add(preKeyRecord);
388									}
389								} catch (InvalidKeyIdException ignored) {
390								}
391							}
392						}
393						int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
394						if (newKeys > 0) {
395							List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
396									axolotlStore.getCurrentPreKeyId() + 1, newKeys);
397							preKeyRecords.addAll(newRecords);
398							for (PreKeyRecord record : newRecords) {
399								axolotlStore.storePreKey(record.getId(), record);
400							}
401							changed = true;
402							Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
403						}
404
405
406						if (changed) {
407							IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
408									signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
409									preKeyRecords, getOwnDeviceId());
410							Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
411							mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
412								@Override
413								public void onIqPacketReceived(Account account, IqPacket packet) {
414									// TODO: implement this!
415									Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet);
416								}
417							});
418						}
419					} catch (InvalidKeyException e) {
420						Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
421						return;
422					}
423				} else {
424					Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing Bundle:" + packet.findChild("error"));
425				}
426			}
427		});
428	}
429
430	public boolean isContactAxolotlCapable(Contact contact) {
431
432		Jid jid = contact.getJid().toBareJid();
433		AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
434		return sessions.hasAny(address) ||
435				(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
436	}
437
438	public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
439		return axolotlStore.getFingerprintTrust(fingerprint);
440	}
441
442	public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
443		axolotlStore.setFingerprintTrust(fingerprint, trust);
444	}
445
446	private void buildSessionFromPEP(final AxolotlAddress address) {
447		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
448
449		try {
450			IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
451					Jid.fromString(address.getName()), address.getDeviceId());
452			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
453			mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
454				private void finish() {
455					AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
456					if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
457							&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
458						mXmppConnectionService.keyStatusUpdated();
459					}
460				}
461
462				@Override
463				public void onIqPacketReceived(Account account, IqPacket packet) {
464					if (packet.getType() == IqPacket.TYPE.RESULT) {
465						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
466						final IqParser parser = mXmppConnectionService.getIqParser();
467						final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
468						final PreKeyBundle bundle = parser.bundle(packet);
469						if (preKeyBundleList.isEmpty() || bundle == null) {
470							Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
471							fetchStatusMap.put(address, FetchStatus.ERROR);
472							finish();
473							return;
474						}
475						Random random = new Random();
476						final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
477						if (preKey == null) {
478							//should never happen
479							fetchStatusMap.put(address, FetchStatus.ERROR);
480							finish();
481							return;
482						}
483
484						final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
485								preKey.getPreKeyId(), preKey.getPreKey(),
486								bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
487								bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
488
489						axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
490
491						try {
492							SessionBuilder builder = new SessionBuilder(axolotlStore, address);
493							builder.process(preKeyBundle);
494							XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
495							sessions.put(address, session);
496							fetchStatusMap.put(address, FetchStatus.SUCCESS);
497						} catch (UntrustedIdentityException | InvalidKeyException e) {
498							Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
499									+ e.getClass().getName() + ", " + e.getMessage());
500							fetchStatusMap.put(address, FetchStatus.ERROR);
501						}
502
503						finish();
504					} else {
505						fetchStatusMap.put(address, FetchStatus.ERROR);
506						Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
507						finish();
508						return;
509					}
510				}
511			});
512		} catch (InvalidJidException e) {
513			Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
514		}
515	}
516
517	public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
518		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
519		Jid contactJid = conversation.getContact().getJid().toBareJid();
520		Set<AxolotlAddress> addresses = new HashSet<>();
521		if (deviceIds.get(contactJid) != null) {
522			for (Integer foreignId : this.deviceIds.get(contactJid)) {
523				AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
524				if (sessions.get(address) == null) {
525					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
526					if (identityKey != null) {
527						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
528						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
529						sessions.put(address, session);
530					} else {
531						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
532						addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
533					}
534				}
535			}
536		} else {
537			Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
538		}
539		if (deviceIds.get(account.getJid().toBareJid()) != null) {
540			for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
541				AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
542				if (sessions.get(address) == null) {
543					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
544					if (identityKey != null) {
545						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
546						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
547						sessions.put(address, session);
548					} else {
549						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
550						addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
551					}
552				}
553			}
554		}
555
556		return addresses;
557	}
558
559	public boolean createSessionsIfNeeded(final Conversation conversation) {
560		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
561		boolean newSessions = false;
562		Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
563		for (AxolotlAddress address : addresses) {
564			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
565			FetchStatus status = fetchStatusMap.get(address);
566			if (status == null || status == FetchStatus.ERROR) {
567				fetchStatusMap.put(address, FetchStatus.PENDING);
568				this.buildSessionFromPEP(address);
569				newSessions = true;
570			} else if (status == FetchStatus.PENDING) {
571				newSessions = true;
572			} else {
573				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
574			}
575		}
576
577		return newSessions;
578	}
579
580	public boolean hasPendingKeyFetches(Conversation conversation) {
581		AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
582		AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
583		return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
584				|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
585
586	}
587
588	@Nullable
589	private XmppAxolotlMessage buildHeader(Contact contact) {
590		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
591				contact.getJid().toBareJid(), getOwnDeviceId());
592
593		Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
594		Set<XmppAxolotlSession> ownSessions = findOwnSessions();
595		if (contactSessions.isEmpty()) {
596			return null;
597		}
598		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
599		for (XmppAxolotlSession session : contactSessions) {
600			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
601			axolotlMessage.addDevice(session);
602		}
603		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
604		for (XmppAxolotlSession session : ownSessions) {
605			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
606			axolotlMessage.addDevice(session);
607		}
608
609		return axolotlMessage;
610	}
611
612	@Nullable
613	public XmppAxolotlMessage encrypt(Message message) {
614		XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
615
616		if (axolotlMessage != null) {
617			final String content;
618			if (message.hasFileOnRemoteHost()) {
619				content = message.getFileParams().url.toString();
620			} else {
621				content = message.getBody();
622			}
623			try {
624				axolotlMessage.encrypt(content);
625			} catch (CryptoFailedException e) {
626				Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
627				return null;
628			}
629		}
630
631		return axolotlMessage;
632	}
633
634	public void preparePayloadMessage(final Message message, final boolean delay) {
635		executor.execute(new Runnable() {
636			@Override
637			public void run() {
638				XmppAxolotlMessage axolotlMessage = encrypt(message);
639				if (axolotlMessage == null) {
640					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
641					//mXmppConnectionService.updateConversationUi();
642				} else {
643					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
644					messageCache.put(message.getUuid(), axolotlMessage);
645					mXmppConnectionService.resendMessage(message, delay);
646				}
647			}
648		});
649	}
650
651	public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
652		executor.execute(new Runnable() {
653			@Override
654			public void run() {
655				XmppAxolotlMessage axolotlMessage = buildHeader(contact);
656				onMessageCreatedCallback.run(axolotlMessage);
657			}
658		});
659	}
660
661	public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
662		XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
663		if (axolotlMessage != null) {
664			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
665			messageCache.remove(message.getUuid());
666		} else {
667			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
668		}
669		return axolotlMessage;
670	}
671
672	private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
673		IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
674		return (identityKey != null)
675				? new XmppAxolotlSession(account, axolotlStore, address,
676						identityKey.getFingerprint().replaceAll("\\s", ""))
677				: null;
678	}
679
680	private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
681		AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
682				message.getSenderDeviceId());
683		XmppAxolotlSession session = sessions.get(senderAddress);
684		if (session == null) {
685			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
686			session = recreateUncachedSession(senderAddress);
687			if (session == null) {
688				session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
689			}
690		}
691		return session;
692	}
693
694	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
695		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
696
697		XmppAxolotlSession session = getReceivingSession(message);
698		try {
699			plaintextMessage = message.decrypt(session, getOwnDeviceId());
700			Integer preKeyId = session.getPreKeyId();
701			if (preKeyId != null) {
702				publishBundlesIfNeeded();
703				session.resetPreKeyId();
704			}
705		} catch (CryptoFailedException e) {
706			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
707		}
708
709		if (session.isFresh() && plaintextMessage != null) {
710			sessions.put(session);
711		}
712
713		return plaintextMessage;
714	}
715
716	public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
717		XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
718
719		XmppAxolotlSession session = getReceivingSession(message);
720		keyTransportMessage = message.getParameters(session, getOwnDeviceId());
721
722		if (session.isFresh() && keyTransportMessage != null) {
723			sessions.put(session);
724		}
725
726		return keyTransportMessage;
727	}
728}