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}