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