AxolotlService.java

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