1package eu.siacs.conversations.crypto.axolotl;
2
3import android.os.Bundle;
4import android.security.KeyChain;
5import android.support.annotation.NonNull;
6import android.support.annotation.Nullable;
7import android.util.Log;
8import android.util.Pair;
9
10import org.bouncycastle.jce.provider.BouncyCastleProvider;
11import org.whispersystems.libsignal.SignalProtocolAddress;
12import org.whispersystems.libsignal.IdentityKey;
13import org.whispersystems.libsignal.IdentityKeyPair;
14import org.whispersystems.libsignal.InvalidKeyException;
15import org.whispersystems.libsignal.InvalidKeyIdException;
16import org.whispersystems.libsignal.SessionBuilder;
17import org.whispersystems.libsignal.UntrustedIdentityException;
18import org.whispersystems.libsignal.ecc.ECPublicKey;
19import org.whispersystems.libsignal.state.PreKeyBundle;
20import org.whispersystems.libsignal.state.PreKeyRecord;
21import org.whispersystems.libsignal.state.SignedPreKeyRecord;
22import org.whispersystems.libsignal.util.KeyHelper;
23
24import java.security.PrivateKey;
25import java.security.Security;
26import java.security.Signature;
27import java.security.cert.X509Certificate;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collection;
31import java.util.Collections;
32import java.util.HashMap;
33import java.util.HashSet;
34import java.util.Iterator;
35import java.util.List;
36import java.util.Map;
37import java.util.Random;
38import java.util.Set;
39import java.util.concurrent.atomic.AtomicBoolean;
40
41import eu.siacs.conversations.Config;
42import eu.siacs.conversations.entities.Account;
43import eu.siacs.conversations.entities.Contact;
44import eu.siacs.conversations.entities.Conversation;
45import eu.siacs.conversations.entities.Message;
46import eu.siacs.conversations.parser.IqParser;
47import eu.siacs.conversations.services.XmppConnectionService;
48import eu.siacs.conversations.utils.CryptoHelper;
49import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
50import eu.siacs.conversations.xml.Element;
51import eu.siacs.conversations.xml.Namespace;
52import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
53import eu.siacs.conversations.xmpp.OnIqPacketReceived;
54import eu.siacs.conversations.xmpp.jid.InvalidJidException;
55import eu.siacs.conversations.xmpp.jid.Jid;
56import eu.siacs.conversations.xmpp.pep.PublishOptions;
57import eu.siacs.conversations.xmpp.stanzas.IqPacket;
58
59public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
60
61 public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
62 public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
63 public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify";
64 public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
65 public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification";
66
67 public static final String LOGPREFIX = "AxolotlService";
68
69 public static final int NUM_KEYS_TO_PUBLISH = 100;
70 public static final int publishTriesThreshold = 3;
71
72 private final Account account;
73 private final XmppConnectionService mXmppConnectionService;
74 private final SQLiteAxolotlStore axolotlStore;
75 private final SessionMap sessions;
76 private final Map<Jid, Set<Integer>> deviceIds;
77 private final Map<String, XmppAxolotlMessage> messageCache;
78 private final FetchStatusMap fetchStatusMap;
79 private final HashMap<Jid,List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
80 private final SerialSingleThreadExecutor executor;
81 private int numPublishTriesOnEmptyPep = 0;
82 private boolean pepBroken = false;
83 private int lastDeviceListNotificationHash = 0;
84
85 private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
86
87 @Override
88 public void onAdvancedStreamFeaturesAvailable(Account account) {
89 if (Config.supportOmemo()
90 && account.getXmppConnection() != null
91 && account.getXmppConnection().getFeatures().pep()) {
92 publishBundlesIfNeeded(true, false);
93 } else {
94 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
95 }
96 }
97
98 public boolean fetchMapHasErrors(List<Jid> jids) {
99 for(Jid jid : jids) {
100 if (deviceIds.get(jid) != null) {
101 for (Integer foreignId : this.deviceIds.get(jid)) {
102 SignalProtocolAddress address = new SignalProtocolAddress(jid.toPreppedString(), foreignId);
103 if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) {
104 return true;
105 }
106 }
107 }
108 }
109 return false;
110 }
111
112 public void preVerifyFingerprint(Contact contact, String fingerprint) {
113 axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
114 }
115
116 public void preVerifyFingerprint(Account account, String fingerprint) {
117 axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint);
118 }
119
120 public boolean hasVerifiedKeys(String name) {
121 for(XmppAxolotlSession session : this.sessions.getAll(name).values()) {
122 if (session.getTrust().isVerified()) {
123 return true;
124 }
125 }
126 return false;
127 }
128
129 private static class AxolotlAddressMap<T> {
130 protected Map<String, Map<Integer, T>> map;
131 protected final Object MAP_LOCK = new Object();
132
133 public AxolotlAddressMap() {
134 this.map = new HashMap<>();
135 }
136
137 public void put(SignalProtocolAddress address, T value) {
138 synchronized (MAP_LOCK) {
139 Map<Integer, T> devices = map.get(address.getName());
140 if (devices == null) {
141 devices = new HashMap<>();
142 map.put(address.getName(), devices);
143 }
144 devices.put(address.getDeviceId(), value);
145 }
146 }
147
148 public T get(SignalProtocolAddress address) {
149 synchronized (MAP_LOCK) {
150 Map<Integer, T> devices = map.get(address.getName());
151 if (devices == null) {
152 return null;
153 }
154 return devices.get(address.getDeviceId());
155 }
156 }
157
158 public Map<Integer, T> getAll(String name) {
159 synchronized (MAP_LOCK) {
160 Map<Integer, T> devices = map.get(name);
161 if (devices == null) {
162 return new HashMap<>();
163 }
164 return devices;
165 }
166 }
167
168 public boolean hasAny(SignalProtocolAddress address) {
169 synchronized (MAP_LOCK) {
170 Map<Integer, T> devices = map.get(address.getName());
171 return devices != null && !devices.isEmpty();
172 }
173 }
174
175 public void clear() {
176 map.clear();
177 }
178
179 }
180
181 private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
182 private final XmppConnectionService xmppConnectionService;
183 private final Account account;
184
185 public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
186 super();
187 this.xmppConnectionService = service;
188 this.account = account;
189 this.fillMap(store);
190 }
191
192 private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
193 for (Integer deviceId : deviceIds) {
194 SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
195 IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
196 if(Config.X509_VERIFICATION) {
197 X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
198 if (certificate != null) {
199 Bundle information = CryptoHelper.extractCertificateInformation(certificate);
200 try {
201 final String cn = information.getString("subject_cn");
202 final Jid jid = Jid.fromString(bareJid);
203 Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
204 account.getRoster().getContact(jid).setCommonName(cn);
205 } catch (final InvalidJidException ignored) {
206 //ignored
207 }
208 }
209 }
210 this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
211 }
212 }
213
214 private void fillMap(SQLiteAxolotlStore store) {
215 List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString());
216 putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store);
217 for (String address : store.getKnownAddresses()) {
218 deviceIds = store.getSubDeviceSessions(address);
219 putDevicesForJid(address, deviceIds, store);
220 }
221 }
222
223 @Override
224 public void put(SignalProtocolAddress address, XmppAxolotlSession value) {
225 super.put(address, value);
226 value.setNotFresh();
227 }
228
229 public void put(XmppAxolotlSession session) {
230 this.put(session.getRemoteAddress(), session);
231 }
232 }
233
234 public enum FetchStatus {
235 PENDING,
236 SUCCESS,
237 SUCCESS_VERIFIED,
238 TIMEOUT,
239 SUCCESS_TRUSTED,
240 ERROR
241 }
242
243 private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
244
245 public void clearErrorFor(Jid jid) {
246 synchronized (MAP_LOCK) {
247 Map<Integer, FetchStatus> devices = this.map.get(jid.toBareJid().toPreppedString());
248 if (devices == null) {
249 return;
250 }
251 for(Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
252 if (entry.getValue() == FetchStatus.ERROR) {
253 Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")");
254 entry.setValue(FetchStatus.TIMEOUT);
255 }
256 }
257 }
258 }
259 }
260
261 public static String getLogprefix(Account account) {
262 return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
263 }
264
265 public AxolotlService(Account account, XmppConnectionService connectionService) {
266 if (account == null || connectionService == null) {
267 throw new IllegalArgumentException("account and service cannot be null");
268 }
269 if (Security.getProvider("BC") == null) {
270 Security.addProvider(new BouncyCastleProvider());
271 }
272 this.mXmppConnectionService = connectionService;
273 this.account = account;
274 this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
275 this.deviceIds = new HashMap<>();
276 this.messageCache = new HashMap<>();
277 this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
278 this.fetchStatusMap = new FetchStatusMap();
279 this.executor = new SerialSingleThreadExecutor("Axolotl");
280 }
281
282 public String getOwnFingerprint() {
283 return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
284 }
285
286 public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
287 return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status);
288 }
289
290 public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
291 return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status);
292 }
293
294 public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
295 Set<IdentityKey> keys = new HashSet<>();
296 for(Jid jid : jids) {
297 keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
298 }
299 return keys;
300 }
301
302 public long getNumTrustedKeys(Jid jid) {
303 return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString());
304 }
305
306 public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
307 for(Jid jid : jids) {
308 if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) {
309 return true;
310 }
311 }
312 return false;
313 }
314
315 private SignalProtocolAddress getAddressForJid(Jid jid) {
316 return new SignalProtocolAddress(jid.toPreppedString(), 0);
317 }
318
319 public Collection<XmppAxolotlSession> findOwnSessions() {
320 SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
321 ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values());
322 Collections.sort(s);
323 return s;
324 }
325
326
327
328 public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
329 SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
330 ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
331 Collections.sort(s);
332 return s;
333 }
334
335 private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
336 HashSet<XmppAxolotlSession> sessions = new HashSet<>();
337 for(Jid jid : conversation.getAcceptedCryptoTargets()) {
338 sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values());
339 }
340 return sessions;
341 }
342
343 private boolean hasAny(Jid jid) {
344 return sessions.hasAny(getAddressForJid(jid));
345 }
346
347 public boolean isPepBroken() {
348 return this.pepBroken;
349 }
350
351 public void resetBrokenness() {
352 this.pepBroken = false;
353 this.numPublishTriesOnEmptyPep = 0;
354 this.lastDeviceListNotificationHash = 0;
355 }
356
357 public void clearErrorsInFetchStatusMap(Jid jid) {
358 fetchStatusMap.clearErrorFor(jid);
359 }
360
361 public void regenerateKeys(boolean wipeOther) {
362 axolotlStore.regenerate();
363 sessions.clear();
364 fetchStatusMap.clear();
365 fetchDeviceIdsMap.clear();
366 publishBundlesIfNeeded(true, wipeOther);
367 }
368
369 public void destroy() {
370 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": destroying old axolotl service. no longer in use");
371 mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
372 }
373
374 public AxolotlService makeNew() {
375 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": make new axolotl service");
376 return new AxolotlService(this.account,this.mXmppConnectionService);
377 }
378
379 public int getOwnDeviceId() {
380 return axolotlStore.getLocalRegistrationId();
381 }
382
383 public SignalProtocolAddress getOwnAxolotlAddress() {
384 return new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId());
385 }
386
387 public Set<Integer> getOwnDeviceIds() {
388 return this.deviceIds.get(account.getJid().toBareJid());
389 }
390
391 public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
392 final int hash = deviceIds.hashCode();
393 final boolean me = jid.toBareJid().equals(account.getJid().toBareJid());
394 if (me) {
395 if (hash != 0 && hash == this.lastDeviceListNotificationHash) {
396 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ignoring duplicate own device id list");
397 return;
398 }
399 this.lastDeviceListNotificationHash = hash;
400 }
401 boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId());
402 if (me) {
403 deviceIds.remove(getOwnDeviceId());
404 }
405 Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
406 expiredDevices.removeAll(deviceIds);
407 for (Integer deviceId : expiredDevices) {
408 SignalProtocolAddress address = new SignalProtocolAddress(jid.toBareJid().toPreppedString(), deviceId);
409 XmppAxolotlSession session = sessions.get(address);
410 if (session != null && session.getFingerprint() != null) {
411 if (session.getTrust().isActive()) {
412 session.setTrust(session.getTrust().toInactive());
413 }
414 }
415 }
416 Set<Integer> newDevices = new HashSet<>(deviceIds);
417 for (Integer deviceId : newDevices) {
418 SignalProtocolAddress address = new SignalProtocolAddress(jid.toBareJid().toPreppedString(), deviceId);
419 XmppAxolotlSession session = sessions.get(address);
420 if (session != null && session.getFingerprint() != null) {
421 if (!session.getTrust().isActive()) {
422 Log.d(Config.LOGTAG,"reactivating device with fingerprint "+session.getFingerprint());
423 session.setTrust(session.getTrust().toActive());
424 }
425 }
426 }
427 if (me) {
428 if (Config.OMEMO_AUTO_EXPIRY != 0) {
429 needsPublishing |= deviceIds.removeAll(getExpiredDevices());
430 }
431 needsPublishing |= this.changeAccessMode.get();
432 for (Integer deviceId : deviceIds) {
433 SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.toBareJid().toPreppedString(), deviceId);
434 if (sessions.get(ownDeviceAddress) == null) {
435 FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
436 if (status == null || status == FetchStatus.TIMEOUT) {
437 fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING);
438 this.buildSessionFromPEP(ownDeviceAddress);
439 }
440 }
441 }
442 if (needsPublishing) {
443 publishOwnDeviceId(deviceIds);
444 }
445 }
446 this.deviceIds.put(jid, deviceIds);
447 mXmppConnectionService.updateConversationUi(); //update the lock icon
448 mXmppConnectionService.keyStatusUpdated(null);
449 }
450
451 public void wipeOtherPepDevices() {
452 if (pepBroken) {
453 Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
454 return;
455 }
456 Set<Integer> deviceIds = new HashSet<>();
457 deviceIds.add(getOwnDeviceId());
458 publishDeviceIdsAndRefineAccessModel(deviceIds);
459 }
460
461 public void distrustFingerprint(final String fingerprint) {
462 final String fp = fingerprint.replaceAll("\\s", "");
463 final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp);
464 axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted());
465 }
466
467 public void publishOwnDeviceIdIfNeeded() {
468 if (pepBroken) {
469 Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
470 return;
471 }
472 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
473 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
474 @Override
475 public void onIqPacketReceived(Account account, IqPacket packet) {
476 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
477 Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
478 } else {
479 Element item = mXmppConnectionService.getIqParser().getItem(packet);
480 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
481 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds);
482 registerDevices(account.getJid().toBareJid(),deviceIds);
483 }
484 }
485 });
486 }
487
488 private Set<Integer> getExpiredDevices() {
489 Set<Integer> devices = new HashSet<>();
490 for(XmppAxolotlSession session : findOwnSessions()) {
491 if (session.getTrust().isActive()) {
492 long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
493 if (diff > Config.OMEMO_AUTO_EXPIRY) {
494 long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
495 long hours = Math.round(lastMessageDiff/(1000*60.0*60.0));
496 if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
497 devices.add(session.getRemoteAddress().getDeviceId());
498 session.setTrust(session.getTrust().toInactive());
499 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago");
500 } else {
501 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago");
502 }
503 }
504 }
505 }
506 return devices;
507 }
508
509 public void publishOwnDeviceId(Set<Integer> deviceIds) {
510 Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
511 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids");
512 if (deviceIdsCopy.isEmpty()) {
513 if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
514 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
515 pepBroken = true;
516 return;
517 } else {
518 numPublishTriesOnEmptyPep++;
519 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
520 }
521 } else {
522 numPublishTriesOnEmptyPep = 0;
523 }
524 deviceIdsCopy.add(getOwnDeviceId());
525 publishDeviceIdsAndRefineAccessModel(deviceIdsCopy);
526 }
527
528 private void publishDeviceIdsAndRefineAccessModel(Set<Integer> ids) {
529 publishDeviceIdsAndRefineAccessModel(ids,true);
530 }
531
532 private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
533 final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
534 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
535 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
536 @Override
537 public void onIqPacketReceived(Account account, IqPacket packet) {
538 Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
539 if (firstAttempt && error != null && error.hasChild("precondition-not-met",Namespace.PUBSUB_ERROR)) {
540 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for device list. pushing node configuration");
541 mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
542 @Override
543 public void onPushSucceeded() {
544 publishDeviceIdsAndRefineAccessModel(ids, false);
545 }
546
547 @Override
548 public void onPushFailed() {
549 publishDeviceIdsAndRefineAccessModel(ids, false);
550 }
551 });
552 } else {
553 if (AxolotlService.this.changeAccessMode.compareAndSet(true,false)) {
554 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done changing access mode");
555 account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,false);
556 mXmppConnectionService.databaseBackend.updateAccount(account);
557 }
558 if (packet.getType() == IqPacket.TYPE.ERROR) {
559 pepBroken = true;
560 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
561 }
562 }
563 }
564 });
565 }
566
567 public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
568 final Set<PreKeyRecord> preKeyRecords,
569 final boolean announceAfter,
570 final boolean wipe) {
571 try {
572 IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
573 PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
574 X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
575 Signature verifier = Signature.getInstance("sha256WithRSA");
576 verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG());
577 verifier.update(axolotlPublicKey.serialize());
578 byte[] signature = verifier.sign();
579 IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
580 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
581 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
582 @Override
583 public void onIqPacketReceived(final Account account, IqPacket packet) {
584 String node = AxolotlService.PEP_VERIFICATION+":"+getOwnDeviceId();
585 mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
586 @Override
587 public void onPushSucceeded() {
588 Log.d(Config.LOGTAG,getLogprefix(account) + "configured verification node to be world readable");
589 publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
590 }
591
592 @Override
593 public void onPushFailed() {
594 Log.d(Config.LOGTAG,getLogprefix(account) + "unable to set access model on verification node");
595 publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
596 }
597 });
598 }
599 });
600 } catch (Exception e) {
601 e.printStackTrace();
602 }
603 }
604
605 public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
606 if (pepBroken) {
607 Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
608 return;
609 }
610
611 if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
612 this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
613 } else {
614 if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,true)) {
615 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server doesn’t support publish-options. setting for later access mode change");
616 mXmppConnectionService.databaseBackend.updateAccount(account);
617 }
618 }
619 if (this.changeAccessMode.get()) {
620 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server gained publish-options capabilities. changing access model");
621 }
622 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
623 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
624 @Override
625 public void onIqPacketReceived(Account account, IqPacket packet) {
626
627 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
628 return; //ignore timeout. do nothing
629 }
630
631 if (packet.getType() == IqPacket.TYPE.ERROR) {
632 Element error = packet.findChild("error");
633 if (error == null || !error.hasChild("item-not-found")) {
634 pepBroken = true;
635 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet);
636 return;
637 }
638 }
639
640 PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
641 Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
642 boolean flush = false;
643 if (bundle == null) {
644 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
645 bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
646 flush = true;
647 }
648 if (keys == null) {
649 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
650 }
651 try {
652 boolean changed = false;
653 // Validate IdentityKey
654 IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
655 if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
656 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
657 changed = true;
658 }
659
660 // Validate signedPreKeyRecord + ID
661 SignedPreKeyRecord signedPreKeyRecord;
662 int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
663 try {
664 signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
665 if (flush
666 || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
667 || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
668 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
669 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
670 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
671 changed = true;
672 }
673 } catch (InvalidKeyIdException e) {
674 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
675 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
676 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
677 changed = true;
678 }
679
680 // Validate PreKeys
681 Set<PreKeyRecord> preKeyRecords = new HashSet<>();
682 if (keys != null) {
683 for (Integer id : keys.keySet()) {
684 try {
685 PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
686 if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
687 preKeyRecords.add(preKeyRecord);
688 }
689 } catch (InvalidKeyIdException ignored) {
690 }
691 }
692 }
693 int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
694 if (newKeys > 0) {
695 List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
696 axolotlStore.getCurrentPreKeyId() + 1, newKeys);
697 preKeyRecords.addAll(newRecords);
698 for (PreKeyRecord record : newRecords) {
699 axolotlStore.storePreKey(record.getId(), record);
700 }
701 changed = true;
702 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
703 }
704
705
706 if (changed || changeAccessMode.get()) {
707 if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
708 mXmppConnectionService.publishDisplayName(account);
709 publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
710 } else {
711 publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
712 }
713 } else {
714 Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
715 if (wipe) {
716 wipeOtherPepDevices();
717 } else if (announce) {
718 Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
719 publishOwnDeviceIdIfNeeded();
720 }
721 }
722 } catch (InvalidKeyException e) {
723 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
724 }
725 }
726 });
727 }
728
729 private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
730 Set<PreKeyRecord> preKeyRecords,
731 final boolean announceAfter,
732 final boolean wipe) {
733 publishDeviceBundle(signedPreKeyRecord,preKeyRecords,announceAfter,wipe,true);
734 }
735
736 private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord,
737 final Set<PreKeyRecord> preKeyRecords,
738 final boolean announceAfter,
739 final boolean wipe,
740 final boolean firstAttempt) {
741 final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
742 IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
743 signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
744 preKeyRecords, getOwnDeviceId(),publishOptions);
745 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing...");
746 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
747 @Override
748 public void onIqPacketReceived(final Account account, IqPacket packet) {
749 Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
750 if (firstAttempt && error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR)) {
751 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for bundle. pushing node configuration");
752 final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
753 mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
754 @Override
755 public void onPushSucceeded() {
756 publishDeviceBundle(signedPreKeyRecord,preKeyRecords, announceAfter, wipe, false);
757 }
758
759 @Override
760 public void onPushFailed() {
761 publishDeviceBundle(signedPreKeyRecord,preKeyRecords, announceAfter, wipe, false);
762 }
763 });
764 } else if (packet.getType() == IqPacket.TYPE.RESULT) {
765 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
766 if (wipe) {
767 wipeOtherPepDevices();
768 } else if (announceAfter) {
769 Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
770 publishOwnDeviceIdIfNeeded();
771 }
772 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
773 pepBroken = true;
774 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
775 }
776 }
777 });
778 }
779
780 public enum AxolotlCapability {
781 FULL,
782 MISSING_PRESENCE,
783 MISSING_KEYS,
784 WRONG_CONFIGURATION,
785 NO_MEMBERS
786 }
787
788 public boolean isConversationAxolotlCapable(Conversation conversation) {
789 return conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().nonanonymous() && conversation.getMucOptions().membersOnly());
790 }
791
792 public Pair<AxolotlCapability,Jid> isConversationAxolotlCapableDetailed(Conversation conversation) {
793 if (conversation.getMode() == Conversation.MODE_SINGLE
794 || (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) {
795 final List<Jid> jids = getCryptoTargets(conversation);
796 for(Jid jid : jids) {
797 if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
798 if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) {
799 return new Pair<>(AxolotlCapability.MISSING_KEYS,jid);
800 } else {
801 return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid);
802 }
803 }
804 }
805 if (jids.size() > 0) {
806 return new Pair<>(AxolotlCapability.FULL, null);
807 } else {
808 return new Pair<>(AxolotlCapability.NO_MEMBERS, null);
809 }
810 } else {
811 return new Pair<>(AxolotlCapability.WRONG_CONFIGURATION, null);
812 }
813 }
814
815 public List<Jid> getCryptoTargets(Conversation conversation) {
816 final List<Jid> jids;
817 if (conversation.getMode() == Conversation.MODE_SINGLE) {
818 jids = new ArrayList<>();
819 jids.add(conversation.getJid().toBareJid());
820 } else {
821 jids = conversation.getMucOptions().getMembers();
822 }
823 return jids;
824 }
825
826 public FingerprintStatus getFingerprintTrust(String fingerprint) {
827 return axolotlStore.getFingerprintStatus(fingerprint);
828 }
829
830 public X509Certificate getFingerprintCertificate(String fingerprint) {
831 return axolotlStore.getFingerprintCertificate(fingerprint);
832 }
833
834 public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
835 axolotlStore.setFingerprintStatus(fingerprint, status);
836 }
837
838 private void verifySessionWithPEP(final XmppAxolotlSession session) {
839 Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
840 final SignalProtocolAddress address = session.getRemoteAddress();
841 final IdentityKey identityKey = session.getIdentityKey();
842 try {
843 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId());
844 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
845 @Override
846 public void onIqPacketReceived(Account account, IqPacket packet) {
847 Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
848 if (verification != null) {
849 try {
850 Signature verifier = Signature.getInstance("sha256WithRSA");
851 verifier.initVerify(verification.first[0]);
852 verifier.update(identityKey.serialize());
853 if (verifier.verify(verification.second)) {
854 try {
855 mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
856 String fingerprint = session.getFingerprint();
857 Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
858 setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
859 axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
860 fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
861 Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
862 try {
863 final String cn = information.getString("subject_cn");
864 final Jid jid = Jid.fromString(address.getName());
865 Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
866 account.getRoster().getContact(jid).setCommonName(cn);
867 } catch (final InvalidJidException ignored) {
868 //ignored
869 }
870 finishBuildingSessionsFromPEP(address);
871 return;
872 } catch (Exception e) {
873 Log.d(Config.LOGTAG,"could not verify certificate");
874 }
875 }
876 } catch (Exception e) {
877 Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
878 }
879 } else {
880 Log.d(Config.LOGTAG,"no verification found");
881 }
882 fetchStatusMap.put(address, FetchStatus.SUCCESS);
883 finishBuildingSessionsFromPEP(address);
884 }
885 });
886 } catch (InvalidJidException e) {
887 fetchStatusMap.put(address, FetchStatus.SUCCESS);
888 finishBuildingSessionsFromPEP(address);
889 }
890 }
891
892 private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
893
894 private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
895 SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), 0);
896 Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress.getName());
897 Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address.getName());
898 if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
899 FetchStatus report = null;
900 if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) {
901 report = FetchStatus.SUCCESS;
902 } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
903 report = FetchStatus.SUCCESS_VERIFIED;
904 } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) {
905 report = FetchStatus.SUCCESS_TRUSTED;
906 } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
907 report = FetchStatus.ERROR;
908 }
909 mXmppConnectionService.keyStatusUpdated(report);
910 }
911 if (Config.REMOVE_BROKEN_DEVICES) {
912 Set<Integer> ownDeviceIds = new HashSet<>(getOwnDeviceIds());
913 boolean publish = false;
914 for (Map.Entry<Integer, FetchStatus> entry : own.entrySet()) {
915 int id = entry.getKey();
916 if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) {
917 publish = true;
918 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error fetching own device with id " + id + ". removing from announcement");
919 }
920 }
921 if (publish) {
922 publishOwnDeviceId(ownDeviceIds);
923 }
924 }
925 }
926
927 public boolean hasEmptyDeviceList(Jid jid) {
928 return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty());
929 }
930
931 public interface OnDeviceIdsFetched {
932 void fetched(Jid jid, Set<Integer> deviceIds);
933 }
934
935 public interface OnMultipleDeviceIdFetched {
936 void fetched();
937 }
938
939 public void fetchDeviceIds(final Jid jid) {
940 fetchDeviceIds(jid,null);
941 }
942
943 public void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) {
944 synchronized (this.fetchDeviceIdsMap) {
945 List<OnDeviceIdsFetched> callbacks = this.fetchDeviceIdsMap.get(jid);
946 if (callbacks != null) {
947 if (callback != null) {
948 callbacks.add(callback);
949 }
950 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for "+jid+" already running. adding callback");
951 } else {
952 callbacks = new ArrayList<>();
953 if (callback != null) {
954 callbacks.add(callback);
955 }
956 this.fetchDeviceIdsMap.put(jid,callbacks);
957 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for " + jid);
958 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid);
959 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
960 @Override
961 public void onIqPacketReceived(Account account, IqPacket packet) {
962 synchronized (fetchDeviceIdsMap) {
963 List<OnDeviceIdsFetched> callbacks = fetchDeviceIdsMap.remove(jid);
964 if (packet.getType() == IqPacket.TYPE.RESULT) {
965 Element item = mXmppConnectionService.getIqParser().getItem(packet);
966 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
967 registerDevices(jid, deviceIds);
968 if (callbacks != null) {
969 for(OnDeviceIdsFetched callback : callbacks) {
970 callback.fetched(jid, deviceIds);
971 }
972 }
973 } else {
974 Log.d(Config.LOGTAG, packet.toString());
975 if (callbacks != null) {
976 for(OnDeviceIdsFetched callback : callbacks) {
977 callback.fetched(jid, null);
978 }
979 }
980 }
981 }
982 }
983 });
984 }
985 }
986 }
987
988 private void fetchDeviceIds(List<Jid> jids, final OnMultipleDeviceIdFetched callback) {
989 final ArrayList<Jid> unfinishedJids = new ArrayList<>(jids);
990 synchronized (unfinishedJids) {
991 for (Jid jid : unfinishedJids) {
992 fetchDeviceIds(jid, new OnDeviceIdsFetched() {
993 @Override
994 public void fetched(Jid jid, Set<Integer> deviceIds) {
995 synchronized (unfinishedJids) {
996 unfinishedJids.remove(jid);
997 if (unfinishedJids.size() == 0 && callback != null) {
998 callback.fetched();
999 }
1000 }
1001 }
1002 });
1003 }
1004 }
1005 }
1006
1007 private void buildSessionFromPEP(final SignalProtocolAddress address) {
1008 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
1009 if (address.equals(getOwnAxolotlAddress())) {
1010 throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
1011 }
1012
1013 try {
1014 IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
1015 Jid.fromString(address.getName()), address.getDeviceId());
1016 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
1017 mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
1018
1019 @Override
1020 public void onIqPacketReceived(Account account, IqPacket packet) {
1021 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1022 fetchStatusMap.put(address, FetchStatus.TIMEOUT);
1023 } else if (packet.getType() == IqPacket.TYPE.RESULT) {
1024 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
1025 final IqParser parser = mXmppConnectionService.getIqParser();
1026 final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
1027 final PreKeyBundle bundle = parser.bundle(packet);
1028 if (preKeyBundleList.isEmpty() || bundle == null) {
1029 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
1030 fetchStatusMap.put(address, FetchStatus.ERROR);
1031 finishBuildingSessionsFromPEP(address);
1032 return;
1033 }
1034 Random random = new Random();
1035 final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
1036 if (preKey == null) {
1037 //should never happen
1038 fetchStatusMap.put(address, FetchStatus.ERROR);
1039 finishBuildingSessionsFromPEP(address);
1040 return;
1041 }
1042
1043 final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
1044 preKey.getPreKeyId(), preKey.getPreKey(),
1045 bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
1046 bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
1047
1048 try {
1049 SessionBuilder builder = new SessionBuilder(axolotlStore, address);
1050 builder.process(preKeyBundle);
1051 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
1052 sessions.put(address, session);
1053 if (Config.X509_VERIFICATION) {
1054 verifySessionWithPEP(session);
1055 } else {
1056 FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize()));
1057 FetchStatus fetchStatus;
1058 if (status != null && status.isVerified()) {
1059 fetchStatus = FetchStatus.SUCCESS_VERIFIED;
1060 } else if (status != null && status.isTrusted()) {
1061 fetchStatus = FetchStatus.SUCCESS_TRUSTED;
1062 } else {
1063 fetchStatus = FetchStatus.SUCCESS;
1064 }
1065 fetchStatusMap.put(address, fetchStatus);
1066 finishBuildingSessionsFromPEP(address);
1067 }
1068 } catch (UntrustedIdentityException | InvalidKeyException e) {
1069 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
1070 + e.getClass().getName() + ", " + e.getMessage());
1071 fetchStatusMap.put(address, FetchStatus.ERROR);
1072 finishBuildingSessionsFromPEP(address);
1073 }
1074 } else {
1075 fetchStatusMap.put(address, FetchStatus.ERROR);
1076 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
1077 finishBuildingSessionsFromPEP(address);
1078 }
1079 }
1080 });
1081 } catch (InvalidJidException e) {
1082 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
1083 }
1084 }
1085
1086 public Set<SignalProtocolAddress> findDevicesWithoutSession(final Conversation conversation) {
1087 Set<SignalProtocolAddress> addresses = new HashSet<>();
1088 for(Jid jid : getCryptoTargets(conversation)) {
1089 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
1090 if (deviceIds.get(jid) != null) {
1091 for (Integer foreignId : this.deviceIds.get(jid)) {
1092 SignalProtocolAddress address = new SignalProtocolAddress(jid.toPreppedString(), foreignId);
1093 if (sessions.get(address) == null) {
1094 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
1095 if (identityKey != null) {
1096 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
1097 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
1098 sessions.put(address, session);
1099 } else {
1100 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
1101 if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
1102 addresses.add(address);
1103 } else {
1104 Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
1105 }
1106 }
1107 }
1108 }
1109 } else {
1110 mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR);
1111 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
1112 }
1113 }
1114 if (deviceIds.get(account.getJid().toBareJid()) != null) {
1115 for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
1116 SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), ownId);
1117 if (sessions.get(address) == null) {
1118 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
1119 if (identityKey != null) {
1120 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
1121 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
1122 sessions.put(address, session);
1123 } else {
1124 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
1125 if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
1126 addresses.add(address);
1127 } else {
1128 Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
1129 }
1130 }
1131 }
1132 }
1133 }
1134
1135 return addresses;
1136 }
1137
1138 public boolean createSessionsIfNeeded(final Conversation conversation) {
1139 final List<Jid> jidsWithEmptyDeviceList = getCryptoTargets(conversation);
1140 for(Iterator<Jid> iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext();) {
1141 final Jid jid = iterator.next();
1142 if (!hasEmptyDeviceList(jid)) {
1143 iterator.remove();
1144 }
1145 }
1146 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": createSessionsIfNeeded() - jids with empty device list: "+jidsWithEmptyDeviceList);
1147 if (jidsWithEmptyDeviceList.size() > 0) {
1148 fetchDeviceIds(jidsWithEmptyDeviceList, new OnMultipleDeviceIdFetched() {
1149 @Override
1150 public void fetched() {
1151 createSessionsIfNeededActual(conversation);
1152 }
1153 });
1154 return true;
1155 } else {
1156 return createSessionsIfNeededActual(conversation);
1157 }
1158 }
1159
1160 private boolean createSessionsIfNeededActual(final Conversation conversation) {
1161 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
1162 boolean newSessions = false;
1163 Set<SignalProtocolAddress> addresses = findDevicesWithoutSession(conversation);
1164 for (SignalProtocolAddress address : addresses) {
1165 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
1166 FetchStatus status = fetchStatusMap.get(address);
1167 if (status == null || status == FetchStatus.TIMEOUT) {
1168 fetchStatusMap.put(address, FetchStatus.PENDING);
1169 this.buildSessionFromPEP(address);
1170 newSessions = true;
1171 } else if (status == FetchStatus.PENDING) {
1172 newSessions = true;
1173 } else {
1174 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
1175 }
1176 }
1177
1178 return newSessions;
1179 }
1180
1181 public boolean trustedSessionVerified(final Conversation conversation) {
1182 Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
1183 sessions.addAll(findOwnSessions());
1184 boolean verified = false;
1185 for(XmppAxolotlSession session : sessions) {
1186 if (session.getTrust().isTrustedAndActive()) {
1187 if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
1188 verified = true;
1189 } else {
1190 return false;
1191 }
1192 }
1193 }
1194 return verified;
1195 }
1196
1197 public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
1198 SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), 0);
1199 if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) {
1200 return true;
1201 }
1202 synchronized (this.fetchDeviceIdsMap) {
1203 for (Jid jid : jids) {
1204 SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.toBareJid().toPreppedString(), 0);
1205 if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) {
1206 return true;
1207 }
1208 }
1209 }
1210 return false;
1211 }
1212
1213 @Nullable
1214 private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation conversation) {
1215 Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
1216 Collection<XmppAxolotlSession> ownSessions = findOwnSessions();
1217 if (remoteSessions.isEmpty()) {
1218 return false;
1219 }
1220 for (XmppAxolotlSession session : remoteSessions) {
1221 axolotlMessage.addDevice(session);
1222 }
1223 for (XmppAxolotlSession session : ownSessions) {
1224 axolotlMessage.addDevice(session);
1225 }
1226
1227 return true;
1228 }
1229
1230 @Nullable
1231 public XmppAxolotlMessage encrypt(Message message) {
1232 final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
1233 final String content;
1234 if (message.hasFileOnRemoteHost()) {
1235 content = message.getFileParams().url.toString();
1236 } else {
1237 content = message.getBody();
1238 }
1239 try {
1240 axolotlMessage.encrypt(content);
1241 } catch (CryptoFailedException e) {
1242 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
1243 return null;
1244 }
1245 if (!buildHeader(axolotlMessage,message.getConversation())) {
1246 return null;
1247 }
1248
1249 return axolotlMessage;
1250 }
1251
1252 public void preparePayloadMessage(final Message message, final boolean delay) {
1253 executor.execute(new Runnable() {
1254 @Override
1255 public void run() {
1256 XmppAxolotlMessage axolotlMessage = encrypt(message);
1257 if (axolotlMessage == null) {
1258 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
1259 //mXmppConnectionService.updateConversationUi();
1260 } else {
1261 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
1262 messageCache.put(message.getUuid(), axolotlMessage);
1263 mXmppConnectionService.resendMessage(message, delay);
1264 }
1265 }
1266 });
1267 }
1268
1269 public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
1270 executor.execute(new Runnable() {
1271 @Override
1272 public void run() {
1273 final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
1274 if (buildHeader(axolotlMessage,conversation)) {
1275 onMessageCreatedCallback.run(axolotlMessage);
1276 } else {
1277 onMessageCreatedCallback.run(null);
1278 }
1279 }
1280 });
1281 }
1282
1283 public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
1284 XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
1285 if (axolotlMessage != null) {
1286 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
1287 messageCache.remove(message.getUuid());
1288 } else {
1289 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
1290 }
1291 return axolotlMessage;
1292 }
1293
1294 private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) {
1295 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
1296 return (identityKey != null)
1297 ? new XmppAxolotlSession(account, axolotlStore, address, identityKey)
1298 : null;
1299 }
1300
1301 private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
1302 SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toPreppedString(),
1303 message.getSenderDeviceId());
1304 XmppAxolotlSession session = sessions.get(senderAddress);
1305 if (session == null) {
1306 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
1307 session = recreateUncachedSession(senderAddress);
1308 if (session == null) {
1309 session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
1310 }
1311 }
1312 return session;
1313 }
1314
1315 public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
1316 XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
1317
1318 XmppAxolotlSession session = getReceivingSession(message);
1319 try {
1320 plaintextMessage = message.decrypt(session, getOwnDeviceId());
1321 Integer preKeyId = session.getPreKeyId();
1322 if (preKeyId != null) {
1323 publishBundlesIfNeeded(false, false);
1324 session.resetPreKeyId();
1325 }
1326 } catch (CryptoFailedException e) {
1327 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage());
1328 }
1329
1330 if (session.isFresh() && plaintextMessage != null) {
1331 putFreshSession(session);
1332 }
1333
1334 return plaintextMessage;
1335 }
1336
1337 public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
1338 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
1339
1340 XmppAxolotlSession session = getReceivingSession(message);
1341 try {
1342 keyTransportMessage = message.getParameters(session, getOwnDeviceId());
1343 } catch (CryptoFailedException e) {
1344 Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage());
1345 keyTransportMessage = null;
1346 }
1347
1348 if (session.isFresh() && keyTransportMessage != null) {
1349 putFreshSession(session);
1350 }
1351
1352 return keyTransportMessage;
1353 }
1354
1355 private void putFreshSession(XmppAxolotlSession session) {
1356 Log.d(Config.LOGTAG,"put fresh session");
1357 sessions.put(session);
1358 if (Config.X509_VERIFICATION) {
1359 if (session.getIdentityKey() != null) {
1360 verifySessionWithPEP(session);
1361 } else {
1362 Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification");
1363 }
1364 }
1365 }
1366}