1package eu.siacs.conversations.crypto.axolotl;
2
3import android.support.annotation.NonNull;
4import android.support.annotation.Nullable;
5import android.util.Log;
6
7import org.bouncycastle.jce.provider.BouncyCastleProvider;
8import org.whispersystems.libaxolotl.AxolotlAddress;
9import org.whispersystems.libaxolotl.IdentityKey;
10import org.whispersystems.libaxolotl.IdentityKeyPair;
11import org.whispersystems.libaxolotl.InvalidKeyException;
12import org.whispersystems.libaxolotl.InvalidKeyIdException;
13import org.whispersystems.libaxolotl.SessionBuilder;
14import org.whispersystems.libaxolotl.UntrustedIdentityException;
15import org.whispersystems.libaxolotl.ecc.ECPublicKey;
16import org.whispersystems.libaxolotl.state.PreKeyBundle;
17import org.whispersystems.libaxolotl.state.PreKeyRecord;
18import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
19import org.whispersystems.libaxolotl.util.KeyHelper;
20
21import java.security.Security;
22import java.util.Arrays;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Map;
27import java.util.Random;
28import java.util.Set;
29
30import eu.siacs.conversations.Config;
31import eu.siacs.conversations.entities.Account;
32import eu.siacs.conversations.entities.Contact;
33import eu.siacs.conversations.entities.Conversation;
34import eu.siacs.conversations.entities.Message;
35import eu.siacs.conversations.parser.IqParser;
36import eu.siacs.conversations.services.XmppConnectionService;
37import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
38import eu.siacs.conversations.xml.Element;
39import eu.siacs.conversations.xmpp.OnIqPacketReceived;
40import eu.siacs.conversations.xmpp.jid.InvalidJidException;
41import eu.siacs.conversations.xmpp.jid.Jid;
42import eu.siacs.conversations.xmpp.stanzas.IqPacket;
43
44public class AxolotlService {
45
46 public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
47 public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
48 public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
49
50 public static final String LOGPREFIX = "AxolotlService";
51
52 public static final int NUM_KEYS_TO_PUBLISH = 100;
53 public static final int publishTriesThreshold = 3;
54
55 private final Account account;
56 private final XmppConnectionService mXmppConnectionService;
57 private final SQLiteAxolotlStore axolotlStore;
58 private final SessionMap sessions;
59 private final Map<Jid, Set<Integer>> deviceIds;
60 private final Map<String, XmppAxolotlMessage> messageCache;
61 private final FetchStatusMap fetchStatusMap;
62 private final SerialSingleThreadExecutor executor;
63 private int numPublishTriesOnEmptyPep = 0;
64 private boolean pepBroken = false;
65
66 private static class AxolotlAddressMap<T> {
67 protected Map<String, Map<Integer, T>> map;
68 protected final Object MAP_LOCK = new Object();
69
70 public AxolotlAddressMap() {
71 this.map = new HashMap<>();
72 }
73
74 public void put(AxolotlAddress address, T value) {
75 synchronized (MAP_LOCK) {
76 Map<Integer, T> devices = map.get(address.getName());
77 if (devices == null) {
78 devices = new HashMap<>();
79 map.put(address.getName(), devices);
80 }
81 devices.put(address.getDeviceId(), value);
82 }
83 }
84
85 public T get(AxolotlAddress address) {
86 synchronized (MAP_LOCK) {
87 Map<Integer, T> devices = map.get(address.getName());
88 if (devices == null) {
89 return null;
90 }
91 return devices.get(address.getDeviceId());
92 }
93 }
94
95 public Map<Integer, T> getAll(AxolotlAddress address) {
96 synchronized (MAP_LOCK) {
97 Map<Integer, T> devices = map.get(address.getName());
98 if (devices == null) {
99 return new HashMap<>();
100 }
101 return devices;
102 }
103 }
104
105 public boolean hasAny(AxolotlAddress address) {
106 synchronized (MAP_LOCK) {
107 Map<Integer, T> devices = map.get(address.getName());
108 return devices != null && !devices.isEmpty();
109 }
110 }
111
112 public void clear() {
113 map.clear();
114 }
115
116 }
117
118 private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
119 private final XmppConnectionService xmppConnectionService;
120 private final Account account;
121
122 public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
123 super();
124 this.xmppConnectionService = service;
125 this.account = account;
126 this.fillMap(store);
127 }
128
129 private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
130 for (Integer deviceId : deviceIds) {
131 AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
132 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
133 String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
134 this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
135 }
136 }
137
138 private void fillMap(SQLiteAxolotlStore store) {
139 List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
140 putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
141 for (Contact contact : account.getRoster().getContacts()) {
142 Jid bareJid = contact.getJid().toBareJid();
143 if (bareJid == null) {
144 continue; // FIXME: handle this?
145 }
146 String address = bareJid.toString();
147 deviceIds = store.getSubDeviceSessions(address);
148 putDevicesForJid(address, deviceIds, store);
149 }
150
151 }
152
153 @Override
154 public void put(AxolotlAddress address, XmppAxolotlSession value) {
155 super.put(address, value);
156 value.setNotFresh();
157 xmppConnectionService.syncRosterToDisk(account);
158 }
159
160 public void put(XmppAxolotlSession session) {
161 this.put(session.getRemoteAddress(), session);
162 }
163 }
164
165 private static enum FetchStatus {
166 PENDING,
167 SUCCESS,
168 ERROR
169 }
170
171 private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
172
173 }
174
175 public static String getLogprefix(Account account) {
176 return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
177 }
178
179 public AxolotlService(Account account, XmppConnectionService connectionService) {
180 if (Security.getProvider("BC") == null) {
181 Security.addProvider(new BouncyCastleProvider());
182 }
183 this.mXmppConnectionService = connectionService;
184 this.account = account;
185 this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
186 this.deviceIds = new HashMap<>();
187 this.messageCache = new HashMap<>();
188 this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
189 this.fetchStatusMap = new FetchStatusMap();
190 this.executor = new SerialSingleThreadExecutor();
191 }
192
193 public IdentityKey getOwnPublicKey() {
194 return axolotlStore.getIdentityKeyPair().getPublicKey();
195 }
196
197 public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
198 return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
199 }
200
201 public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
202 return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
203 }
204
205 public long getNumTrustedKeys(Contact contact) {
206 return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
207 }
208
209 private AxolotlAddress getAddressForJid(Jid jid) {
210 return new AxolotlAddress(jid.toString(), 0);
211 }
212
213 private Set<XmppAxolotlSession> findOwnSessions() {
214 AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
215 Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
216 return ownDeviceSessions;
217 }
218
219 private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
220 AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
221 Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
222 return sessions;
223 }
224
225 private boolean hasAny(Contact contact) {
226 AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
227 return sessions.hasAny(contactAddress);
228 }
229
230 public void regenerateKeys() {
231 axolotlStore.regenerate();
232 sessions.clear();
233 fetchStatusMap.clear();
234 publishBundlesIfNeeded();
235 publishOwnDeviceIdIfNeeded();
236 }
237
238 public int getOwnDeviceId() {
239 return axolotlStore.getLocalRegistrationId();
240 }
241
242 public Set<Integer> getOwnDeviceIds() {
243 return this.deviceIds.get(account.getJid().toBareJid());
244 }
245
246 private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
247 final XmppAxolotlSession.Trust from,
248 final XmppAxolotlSession.Trust to) {
249 for (Integer deviceId : deviceIds) {
250 AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
251 XmppAxolotlSession session = sessions.get(address);
252 if (session != null && session.getFingerprint() != null
253 && session.getTrust() == from) {
254 session.setTrust(to);
255 }
256 }
257 }
258
259 public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
260 if (jid.toBareJid().equals(account.getJid().toBareJid())) {
261 if (!deviceIds.isEmpty()) {
262 Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status.");
263 pepBroken = false;
264 numPublishTriesOnEmptyPep = 0;
265 }
266 if (deviceIds.contains(getOwnDeviceId())) {
267 deviceIds.remove(getOwnDeviceId());
268 } else {
269 publishOwnDeviceId(deviceIds);
270 }
271 for (Integer deviceId : deviceIds) {
272 AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
273 if (sessions.get(ownDeviceAddress) == null) {
274 buildSessionFromPEP(ownDeviceAddress);
275 }
276 }
277 }
278 Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
279 expiredDevices.removeAll(deviceIds);
280 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
281 XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
282 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
283 XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
284 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
285 XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
286 Set<Integer> newDevices = new HashSet<>(deviceIds);
287 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
288 XmppAxolotlSession.Trust.TRUSTED);
289 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
290 XmppAxolotlSession.Trust.UNDECIDED);
291 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
292 XmppAxolotlSession.Trust.UNTRUSTED);
293 this.deviceIds.put(jid, deviceIds);
294 mXmppConnectionService.keyStatusUpdated();
295 }
296
297 public void wipeOtherPepDevices() {
298 if (pepBroken) {
299 Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
300 return;
301 }
302 Set<Integer> deviceIds = new HashSet<>();
303 deviceIds.add(getOwnDeviceId());
304 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
305 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
306 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
307 @Override
308 public void onIqPacketReceived(Account account, IqPacket packet) {
309 // TODO: implement this!
310 }
311 });
312 }
313
314 public void purgeKey(IdentityKey identityKey) {
315 axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
316 }
317
318 public void publishOwnDeviceIdIfNeeded() {
319 if (pepBroken) {
320 Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
321 return;
322 }
323 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
324 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
325 @Override
326 public void onIqPacketReceived(Account account, IqPacket packet) {
327 if (packet.getType() == IqPacket.TYPE.RESULT) {
328 Element item = mXmppConnectionService.getIqParser().getItem(packet);
329 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
330 if (!deviceIds.contains(getOwnDeviceId())) {
331 publishOwnDeviceId(deviceIds);
332 }
333 } else {
334 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while retrieving Device Ids" + packet.findChild("error"));
335 }
336 }
337 });
338 }
339
340 public void publishOwnDeviceId(Set<Integer> deviceIds) {
341 if (!deviceIds.contains(getOwnDeviceId())) {
342 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
343 if (deviceIds.isEmpty()) {
344 if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
345 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
346 pepBroken = true;
347 return;
348 } else {
349 numPublishTriesOnEmptyPep++;
350 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
351 }
352 } else {
353 numPublishTriesOnEmptyPep = 0;
354 }
355 deviceIds.add(getOwnDeviceId());
356 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
357 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
358 @Override
359 public void onIqPacketReceived(Account account, IqPacket packet) {
360 if (packet.getType() != IqPacket.TYPE.RESULT) {
361 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
362 }
363 }
364 });
365 }
366 }
367
368 public void publishBundlesIfNeeded() {
369 if (!pepBroken) {
370 Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
371 return;
372 }
373 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
374 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
375 @Override
376 public void onIqPacketReceived(Account account, IqPacket packet) {
377 if (packet.getType() == IqPacket.TYPE.RESULT) {
378 PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
379 Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
380 boolean flush = false;
381 if (bundle == null) {
382 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
383 bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
384 flush = true;
385 }
386 if (keys == null) {
387 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
388 }
389 try {
390 boolean changed = false;
391 // Validate IdentityKey
392 IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
393 if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
394 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
395 changed = true;
396 }
397
398 // Validate signedPreKeyRecord + ID
399 SignedPreKeyRecord signedPreKeyRecord;
400 int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
401 try {
402 signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
403 if (flush
404 || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
405 || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
406 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
407 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
408 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
409 changed = true;
410 }
411 } catch (InvalidKeyIdException e) {
412 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
413 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
414 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
415 changed = true;
416 }
417
418 // Validate PreKeys
419 Set<PreKeyRecord> preKeyRecords = new HashSet<>();
420 if (keys != null) {
421 for (Integer id : keys.keySet()) {
422 try {
423 PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
424 if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
425 preKeyRecords.add(preKeyRecord);
426 }
427 } catch (InvalidKeyIdException ignored) {
428 }
429 }
430 }
431 int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
432 if (newKeys > 0) {
433 List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
434 axolotlStore.getCurrentPreKeyId() + 1, newKeys);
435 preKeyRecords.addAll(newRecords);
436 for (PreKeyRecord record : newRecords) {
437 axolotlStore.storePreKey(record.getId(), record);
438 }
439 changed = true;
440 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
441 }
442
443
444 if (changed) {
445 IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
446 signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
447 preKeyRecords, getOwnDeviceId());
448 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
449 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
450 @Override
451 public void onIqPacketReceived(Account account, IqPacket packet) {
452 // TODO: implement this!
453 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet);
454 }
455 });
456 }
457 } catch (InvalidKeyException e) {
458 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
459 return;
460 }
461 } else {
462 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing Bundle:" + packet.findChild("error"));
463 }
464 }
465 });
466 }
467
468 public boolean isContactAxolotlCapable(Contact contact) {
469
470 Jid jid = contact.getJid().toBareJid();
471 AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
472 return sessions.hasAny(address) ||
473 (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
474 }
475
476 public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
477 return axolotlStore.getFingerprintTrust(fingerprint);
478 }
479
480 public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
481 axolotlStore.setFingerprintTrust(fingerprint, trust);
482 }
483
484 private void buildSessionFromPEP(final AxolotlAddress address) {
485 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
486
487 try {
488 IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
489 Jid.fromString(address.getName()), address.getDeviceId());
490 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
491 mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
492 private void finish() {
493 AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
494 if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
495 && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
496 mXmppConnectionService.keyStatusUpdated();
497 }
498 }
499
500 @Override
501 public void onIqPacketReceived(Account account, IqPacket packet) {
502 if (packet.getType() == IqPacket.TYPE.RESULT) {
503 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
504 final IqParser parser = mXmppConnectionService.getIqParser();
505 final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
506 final PreKeyBundle bundle = parser.bundle(packet);
507 if (preKeyBundleList.isEmpty() || bundle == null) {
508 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
509 fetchStatusMap.put(address, FetchStatus.ERROR);
510 finish();
511 return;
512 }
513 Random random = new Random();
514 final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
515 if (preKey == null) {
516 //should never happen
517 fetchStatusMap.put(address, FetchStatus.ERROR);
518 finish();
519 return;
520 }
521
522 final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
523 preKey.getPreKeyId(), preKey.getPreKey(),
524 bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
525 bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
526
527 axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
528
529 try {
530 SessionBuilder builder = new SessionBuilder(axolotlStore, address);
531 builder.process(preKeyBundle);
532 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
533 sessions.put(address, session);
534 fetchStatusMap.put(address, FetchStatus.SUCCESS);
535 } catch (UntrustedIdentityException | InvalidKeyException e) {
536 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
537 + e.getClass().getName() + ", " + e.getMessage());
538 fetchStatusMap.put(address, FetchStatus.ERROR);
539 }
540
541 finish();
542 } else {
543 fetchStatusMap.put(address, FetchStatus.ERROR);
544 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
545 finish();
546 return;
547 }
548 }
549 });
550 } catch (InvalidJidException e) {
551 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
552 }
553 }
554
555 public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
556 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
557 Jid contactJid = conversation.getContact().getJid().toBareJid();
558 Set<AxolotlAddress> addresses = new HashSet<>();
559 if (deviceIds.get(contactJid) != null) {
560 for (Integer foreignId : this.deviceIds.get(contactJid)) {
561 AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
562 if (sessions.get(address) == null) {
563 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
564 if (identityKey != null) {
565 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
566 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
567 sessions.put(address, session);
568 } else {
569 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
570 addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
571 }
572 }
573 }
574 } else {
575 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
576 }
577 if (deviceIds.get(account.getJid().toBareJid()) != null) {
578 for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
579 AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
580 if (sessions.get(address) == null) {
581 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
582 if (identityKey != null) {
583 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
584 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
585 sessions.put(address, session);
586 } else {
587 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
588 addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
589 }
590 }
591 }
592 }
593
594 return addresses;
595 }
596
597 public boolean createSessionsIfNeeded(final Conversation conversation) {
598 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
599 boolean newSessions = false;
600 Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
601 for (AxolotlAddress address : addresses) {
602 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
603 FetchStatus status = fetchStatusMap.get(address);
604 if (status == null || status == FetchStatus.ERROR) {
605 fetchStatusMap.put(address, FetchStatus.PENDING);
606 this.buildSessionFromPEP(address);
607 newSessions = true;
608 } else if (status == FetchStatus.PENDING) {
609 newSessions = true;
610 } else {
611 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
612 }
613 }
614
615 return newSessions;
616 }
617
618 public boolean hasPendingKeyFetches(Conversation conversation) {
619 AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
620 AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
621 return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
622 || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
623
624 }
625
626 @Nullable
627 private XmppAxolotlMessage buildHeader(Contact contact) {
628 final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
629 contact.getJid().toBareJid(), getOwnDeviceId());
630
631 Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
632 Set<XmppAxolotlSession> ownSessions = findOwnSessions();
633 if (contactSessions.isEmpty()) {
634 return null;
635 }
636 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
637 for (XmppAxolotlSession session : contactSessions) {
638 Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
639 axolotlMessage.addDevice(session);
640 }
641 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
642 for (XmppAxolotlSession session : ownSessions) {
643 Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
644 axolotlMessage.addDevice(session);
645 }
646
647 return axolotlMessage;
648 }
649
650 @Nullable
651 public XmppAxolotlMessage encrypt(Message message) {
652 XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
653
654 if (axolotlMessage != null) {
655 final String content;
656 if (message.hasFileOnRemoteHost()) {
657 content = message.getFileParams().url.toString();
658 } else {
659 content = message.getBody();
660 }
661 try {
662 axolotlMessage.encrypt(content);
663 } catch (CryptoFailedException e) {
664 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
665 return null;
666 }
667 }
668
669 return axolotlMessage;
670 }
671
672 public void preparePayloadMessage(final Message message, final boolean delay) {
673 executor.execute(new Runnable() {
674 @Override
675 public void run() {
676 XmppAxolotlMessage axolotlMessage = encrypt(message);
677 if (axolotlMessage == null) {
678 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
679 //mXmppConnectionService.updateConversationUi();
680 } else {
681 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
682 messageCache.put(message.getUuid(), axolotlMessage);
683 mXmppConnectionService.resendMessage(message, delay);
684 }
685 }
686 });
687 }
688
689 public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
690 executor.execute(new Runnable() {
691 @Override
692 public void run() {
693 XmppAxolotlMessage axolotlMessage = buildHeader(contact);
694 onMessageCreatedCallback.run(axolotlMessage);
695 }
696 });
697 }
698
699 public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
700 XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
701 if (axolotlMessage != null) {
702 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
703 messageCache.remove(message.getUuid());
704 } else {
705 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
706 }
707 return axolotlMessage;
708 }
709
710 private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
711 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
712 return (identityKey != null)
713 ? new XmppAxolotlSession(account, axolotlStore, address,
714 identityKey.getFingerprint().replaceAll("\\s", ""))
715 : null;
716 }
717
718 private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
719 AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
720 message.getSenderDeviceId());
721 XmppAxolotlSession session = sessions.get(senderAddress);
722 if (session == null) {
723 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
724 session = recreateUncachedSession(senderAddress);
725 if (session == null) {
726 session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
727 }
728 }
729 return session;
730 }
731
732 public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
733 XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
734
735 XmppAxolotlSession session = getReceivingSession(message);
736 try {
737 plaintextMessage = message.decrypt(session, getOwnDeviceId());
738 Integer preKeyId = session.getPreKeyId();
739 if (preKeyId != null) {
740 publishBundlesIfNeeded();
741 session.resetPreKeyId();
742 }
743 } catch (CryptoFailedException e) {
744 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
745 }
746
747 if (session.isFresh() && plaintextMessage != null) {
748 sessions.put(session);
749 }
750
751 return plaintextMessage;
752 }
753
754 public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
755 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
756
757 XmppAxolotlSession session = getReceivingSession(message);
758 keyTransportMessage = message.getParameters(session, getOwnDeviceId());
759
760 if (session.isFresh() && keyTransportMessage != null) {
761 sessions.put(session);
762 }
763
764 return keyTransportMessage;
765 }
766}