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 (KeyChainException e) {
413			e.printStackTrace();
414		} catch (InterruptedException e) {
415			e.printStackTrace();
416		} catch (NoSuchAlgorithmException e) {
417			Log.d(Config.LOGTAG,"no such algo "+e.getMessage());
418			e.printStackTrace();
419		} catch (java.security.InvalidKeyException e) {
420			e.printStackTrace();
421		} catch (SignatureException e) {
422			e.printStackTrace();
423		}
424
425	}
426
427	public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
428		if (pepBroken) {
429			Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
430			return;
431		}
432		IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
433		mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
434			@Override
435			public void onIqPacketReceived(Account account, IqPacket packet) {
436
437				if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
438					return; //ignore timeout. do nothing
439				}
440
441				if (packet.getType() == IqPacket.TYPE.ERROR) {
442					Element error = packet.findChild("error");
443					if (error == null || !error.hasChild("item-not-found")) {
444						pepBroken = true;
445						Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet);
446						return;
447					}
448				}
449
450				PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
451				Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
452				boolean flush = false;
453				if (bundle == null) {
454					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
455					bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
456					flush = true;
457				}
458				if (keys == null) {
459					Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
460				}
461				try {
462					boolean changed = false;
463					// Validate IdentityKey
464					IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
465					if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
466						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
467						changed = true;
468					}
469
470					// Validate signedPreKeyRecord + ID
471					SignedPreKeyRecord signedPreKeyRecord;
472					int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
473					try {
474						signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
475						if (flush
476								|| !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
477								|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
478							Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
479							signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
480							axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
481							changed = true;
482						}
483					} catch (InvalidKeyIdException e) {
484						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
485						signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
486						axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
487						changed = true;
488					}
489
490					// Validate PreKeys
491					Set<PreKeyRecord> preKeyRecords = new HashSet<>();
492					if (keys != null) {
493						for (Integer id : keys.keySet()) {
494							try {
495								PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
496								if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
497									preKeyRecords.add(preKeyRecord);
498								}
499							} catch (InvalidKeyIdException ignored) {
500							}
501						}
502					}
503					int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
504					if (newKeys > 0) {
505						List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
506								axolotlStore.getCurrentPreKeyId() + 1, newKeys);
507						preKeyRecords.addAll(newRecords);
508						for (PreKeyRecord record : newRecords) {
509							axolotlStore.storePreKey(record.getId(), record);
510						}
511						changed = true;
512						Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
513					}
514
515
516					if (changed) {
517						if (account.getPrivateKeyAlias() == null) {
518							publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
519						} else {
520							publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
521						}
522					} else {
523						Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
524						if (wipe) {
525							wipeOtherPepDevices();
526						} else if (announce) {
527							Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
528							publishOwnDeviceIdIfNeeded();
529						}
530					}
531				} catch (InvalidKeyException e) {
532					Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
533					return;
534				}
535			}
536		});
537	}
538
539	private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
540									 Set<PreKeyRecord> preKeyRecords,
541									 final boolean announceAfter,
542									 final boolean wipe) {
543		IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
544				signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
545				preKeyRecords, getOwnDeviceId());
546		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
547		mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
548			@Override
549			public void onIqPacketReceived(Account account, IqPacket packet) {
550				if (packet.getType() == IqPacket.TYPE.RESULT) {
551					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
552					if (wipe) {
553						wipeOtherPepDevices();
554					} else if (announceAfter) {
555						Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
556						publishOwnDeviceIdIfNeeded();
557					}
558				} else {
559					Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
560				}
561			}
562		});
563	}
564
565	public boolean isContactAxolotlCapable(Contact contact) {
566		Jid jid = contact.getJid().toBareJid();
567		return hasAny(contact) ||
568				(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
569	}
570
571	public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
572		return axolotlStore.getFingerprintTrust(fingerprint);
573	}
574
575	public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
576		axolotlStore.setFingerprintTrust(fingerprint, trust);
577	}
578
579	private void buildSessionFromPEP(final AxolotlAddress address) {
580		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
581		if (address.getDeviceId() == getOwnDeviceId()) {
582			throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
583		}
584
585		try {
586			IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
587					Jid.fromString(address.getName()), address.getDeviceId());
588			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
589			mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
590				private void finish() {
591					AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
592					if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
593							&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
594						mXmppConnectionService.keyStatusUpdated();
595					}
596				}
597
598				@Override
599				public void onIqPacketReceived(Account account, IqPacket packet) {
600					if (packet.getType() == IqPacket.TYPE.RESULT) {
601						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
602						final IqParser parser = mXmppConnectionService.getIqParser();
603						final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
604						final PreKeyBundle bundle = parser.bundle(packet);
605						if (preKeyBundleList.isEmpty() || bundle == null) {
606							Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
607							fetchStatusMap.put(address, FetchStatus.ERROR);
608							finish();
609							return;
610						}
611						Random random = new Random();
612						final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
613						if (preKey == null) {
614							//should never happen
615							fetchStatusMap.put(address, FetchStatus.ERROR);
616							finish();
617							return;
618						}
619
620						final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
621								preKey.getPreKeyId(), preKey.getPreKey(),
622								bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
623								bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
624
625						try {
626							SessionBuilder builder = new SessionBuilder(axolotlStore, address);
627							builder.process(preKeyBundle);
628							XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
629							sessions.put(address, session);
630							fetchStatusMap.put(address, FetchStatus.SUCCESS);
631						} catch (UntrustedIdentityException | InvalidKeyException e) {
632							Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
633									+ e.getClass().getName() + ", " + e.getMessage());
634							fetchStatusMap.put(address, FetchStatus.ERROR);
635						}
636
637						finish();
638					} else {
639						fetchStatusMap.put(address, FetchStatus.ERROR);
640						Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
641						finish();
642						return;
643					}
644				}
645			});
646		} catch (InvalidJidException e) {
647			Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
648		}
649	}
650
651	public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
652		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
653		Jid contactJid = conversation.getContact().getJid().toBareJid();
654		Set<AxolotlAddress> addresses = new HashSet<>();
655		if (deviceIds.get(contactJid) != null) {
656			for (Integer foreignId : this.deviceIds.get(contactJid)) {
657				AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
658				if (sessions.get(address) == null) {
659					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
660					if (identityKey != null) {
661						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
662						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
663						sessions.put(address, session);
664					} else {
665						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
666						addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
667					}
668				}
669			}
670		} else {
671			Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
672		}
673		if (deviceIds.get(account.getJid().toBareJid()) != null) {
674			for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
675				AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
676				if (sessions.get(address) == null) {
677					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
678					if (identityKey != null) {
679						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
680						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
681						sessions.put(address, session);
682					} else {
683						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
684						addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
685					}
686				}
687			}
688		}
689
690		return addresses;
691	}
692
693	public boolean createSessionsIfNeeded(final Conversation conversation) {
694		Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
695		boolean newSessions = false;
696		Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
697		for (AxolotlAddress address : addresses) {
698			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
699			FetchStatus status = fetchStatusMap.get(address);
700			if (status == null || status == FetchStatus.ERROR) {
701				fetchStatusMap.put(address, FetchStatus.PENDING);
702				this.buildSessionFromPEP(address);
703				newSessions = true;
704			} else if (status == FetchStatus.PENDING) {
705				newSessions = true;
706			} else {
707				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
708			}
709		}
710
711		return newSessions;
712	}
713
714	public boolean hasPendingKeyFetches(Conversation conversation) {
715		AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
716		AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
717		return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
718				|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
719
720	}
721
722	@Nullable
723	private XmppAxolotlMessage buildHeader(Contact contact) {
724		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
725				contact.getJid().toBareJid(), getOwnDeviceId());
726
727		Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
728		Set<XmppAxolotlSession> ownSessions = findOwnSessions();
729		if (contactSessions.isEmpty()) {
730			return null;
731		}
732		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
733		for (XmppAxolotlSession session : contactSessions) {
734			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
735			axolotlMessage.addDevice(session);
736		}
737		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
738		for (XmppAxolotlSession session : ownSessions) {
739			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
740			axolotlMessage.addDevice(session);
741		}
742
743		return axolotlMessage;
744	}
745
746	@Nullable
747	public XmppAxolotlMessage encrypt(Message message) {
748		XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
749
750		if (axolotlMessage != null) {
751			final String content;
752			if (message.hasFileOnRemoteHost()) {
753				content = message.getFileParams().url.toString();
754			} else {
755				content = message.getBody();
756			}
757			try {
758				axolotlMessage.encrypt(content);
759			} catch (CryptoFailedException e) {
760				Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
761				return null;
762			}
763		}
764
765		return axolotlMessage;
766	}
767
768	public void preparePayloadMessage(final Message message, final boolean delay) {
769		executor.execute(new Runnable() {
770			@Override
771			public void run() {
772				XmppAxolotlMessage axolotlMessage = encrypt(message);
773				if (axolotlMessage == null) {
774					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
775					//mXmppConnectionService.updateConversationUi();
776				} else {
777					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
778					messageCache.put(message.getUuid(), axolotlMessage);
779					mXmppConnectionService.resendMessage(message, delay);
780				}
781			}
782		});
783	}
784
785	public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
786		executor.execute(new Runnable() {
787			@Override
788			public void run() {
789				XmppAxolotlMessage axolotlMessage = buildHeader(contact);
790				onMessageCreatedCallback.run(axolotlMessage);
791			}
792		});
793	}
794
795	public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
796		XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
797		if (axolotlMessage != null) {
798			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
799			messageCache.remove(message.getUuid());
800		} else {
801			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
802		}
803		return axolotlMessage;
804	}
805
806	private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
807		IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
808		return (identityKey != null)
809				? new XmppAxolotlSession(account, axolotlStore, address,
810						identityKey.getFingerprint().replaceAll("\\s", ""))
811				: null;
812	}
813
814	private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
815		AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
816				message.getSenderDeviceId());
817		XmppAxolotlSession session = sessions.get(senderAddress);
818		if (session == null) {
819			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
820			session = recreateUncachedSession(senderAddress);
821			if (session == null) {
822				session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
823			}
824		}
825		return session;
826	}
827
828	public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
829		XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
830
831		XmppAxolotlSession session = getReceivingSession(message);
832		try {
833			plaintextMessage = message.decrypt(session, getOwnDeviceId());
834			Integer preKeyId = session.getPreKeyId();
835			if (preKeyId != null) {
836				publishBundlesIfNeeded(false, false);
837				session.resetPreKeyId();
838			}
839		} catch (CryptoFailedException e) {
840			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
841		}
842
843		if (session.isFresh() && plaintextMessage != null) {
844			sessions.put(session);
845		}
846
847		return plaintextMessage;
848	}
849
850	public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
851		XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
852
853		XmppAxolotlSession session = getReceivingSession(message);
854		keyTransportMessage = message.getParameters(session, getOwnDeviceId());
855
856		if (session.isFresh() && keyTransportMessage != null) {
857			sessions.put(session);
858		}
859
860		return keyTransportMessage;
861	}
862}