AxolotlService.java

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