1package eu.siacs.conversations.crypto.axolotl;
2
3import android.support.annotation.NonNull;
4import android.support.annotation.Nullable;
5import android.util.Base64;
6import android.util.Log;
7
8import org.whispersystems.libaxolotl.AxolotlAddress;
9import org.whispersystems.libaxolotl.DuplicateMessageException;
10import org.whispersystems.libaxolotl.IdentityKey;
11import org.whispersystems.libaxolotl.IdentityKeyPair;
12import org.whispersystems.libaxolotl.InvalidKeyException;
13import org.whispersystems.libaxolotl.InvalidKeyIdException;
14import org.whispersystems.libaxolotl.InvalidMessageException;
15import org.whispersystems.libaxolotl.InvalidVersionException;
16import org.whispersystems.libaxolotl.LegacyMessageException;
17import org.whispersystems.libaxolotl.NoSessionException;
18import org.whispersystems.libaxolotl.SessionBuilder;
19import org.whispersystems.libaxolotl.SessionCipher;
20import org.whispersystems.libaxolotl.UntrustedIdentityException;
21import org.whispersystems.libaxolotl.ecc.Curve;
22import org.whispersystems.libaxolotl.ecc.ECKeyPair;
23import org.whispersystems.libaxolotl.ecc.ECPublicKey;
24import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
25import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
26import org.whispersystems.libaxolotl.protocol.WhisperMessage;
27import org.whispersystems.libaxolotl.state.AxolotlStore;
28import org.whispersystems.libaxolotl.state.PreKeyBundle;
29import org.whispersystems.libaxolotl.state.PreKeyRecord;
30import org.whispersystems.libaxolotl.state.SessionRecord;
31import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
32import org.whispersystems.libaxolotl.util.KeyHelper;
33
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.HashMap;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Map;
40import java.util.Random;
41import java.util.Set;
42
43import eu.siacs.conversations.Config;
44import eu.siacs.conversations.entities.Account;
45import eu.siacs.conversations.entities.Contact;
46import eu.siacs.conversations.entities.Conversation;
47import eu.siacs.conversations.entities.Message;
48import eu.siacs.conversations.parser.IqParser;
49import eu.siacs.conversations.services.XmppConnectionService;
50import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
51import eu.siacs.conversations.xml.Element;
52import eu.siacs.conversations.xmpp.OnIqPacketReceived;
53import eu.siacs.conversations.xmpp.jid.InvalidJidException;
54import eu.siacs.conversations.xmpp.jid.Jid;
55import eu.siacs.conversations.xmpp.stanzas.IqPacket;
56import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
57
58public class AxolotlService {
59
60 public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
61 public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
62 public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
63
64 public static final int NUM_KEYS_TO_PUBLISH = 10;
65
66 private final Account account;
67 private final XmppConnectionService mXmppConnectionService;
68 private final SQLiteAxolotlStore axolotlStore;
69 private final SessionMap sessions;
70 private final Map<Jid, Set<Integer>> deviceIds;
71 private final Map<String, MessagePacket> messageCache;
72 private final FetchStatusMap fetchStatusMap;
73 private final SerialSingleThreadExecutor executor;
74 private int ownDeviceId;
75
76 public static class SQLiteAxolotlStore implements AxolotlStore {
77
78 public static final String PREKEY_TABLENAME = "prekeys";
79 public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
80 public static final String SESSION_TABLENAME = "sessions";
81 public static final String IDENTITIES_TABLENAME = "identities";
82 public static final String ACCOUNT = "account";
83 public static final String DEVICE_ID = "device_id";
84 public static final String ID = "id";
85 public static final String KEY = "key";
86 public static final String NAME = "name";
87 public static final String TRUSTED = "trusted";
88 public static final String OWN = "ownkey";
89
90 public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key";
91 public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
92 public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
93
94 private final Account account;
95 private final XmppConnectionService mXmppConnectionService;
96
97 private IdentityKeyPair identityKeyPair;
98 private final int localRegistrationId;
99 private int currentPreKeyId = 0;
100
101
102 private static IdentityKeyPair generateIdentityKeyPair() {
103 Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair...");
104 ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
105 IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
106 identityKeyPairKeys.getPrivateKey());
107 return ownKey;
108 }
109
110 private static int generateRegistrationId() {
111 Log.d(Config.LOGTAG, "Generating axolotl registration ID...");
112 int reg_id = KeyHelper.generateRegistrationId(false);
113 return reg_id;
114 }
115
116 public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
117 this.account = account;
118 this.mXmppConnectionService = service;
119 this.localRegistrationId = loadRegistrationId();
120 this.currentPreKeyId = loadCurrentPreKeyId();
121 for (SignedPreKeyRecord record : loadSignedPreKeys()) {
122 Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId());
123 }
124 }
125
126 public int getCurrentPreKeyId() {
127 return currentPreKeyId;
128 }
129
130 // --------------------------------------
131 // IdentityKeyStore
132 // --------------------------------------
133
134 private IdentityKeyPair loadIdentityKeyPair() {
135 String ownName = account.getJid().toBareJid().toString();
136 IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
137 ownName);
138
139 if (ownKey != null) {
140 return ownKey;
141 } else {
142 Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + ownName);
143 ownKey = generateIdentityKeyPair();
144 mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
145 }
146 return ownKey;
147 }
148
149 private int loadRegistrationId() {
150 String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
151 int reg_id;
152 if (regIdString != null) {
153 reg_id = Integer.valueOf(regIdString);
154 } else {
155 Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid());
156 reg_id = generateRegistrationId();
157 boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
158 if (success) {
159 mXmppConnectionService.databaseBackend.updateAccount(account);
160 } else {
161 Log.e(Config.LOGTAG, "Failed to write new key to the database!");
162 }
163 }
164 return reg_id;
165 }
166
167 private int loadCurrentPreKeyId() {
168 String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
169 int reg_id;
170 if (regIdString != null) {
171 reg_id = Integer.valueOf(regIdString);
172 } else {
173 Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid());
174 reg_id = 0;
175 }
176 return reg_id;
177 }
178
179 public void regenerate() {
180 mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
181 account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
182 identityKeyPair = loadIdentityKeyPair();
183 currentPreKeyId = 0;
184 mXmppConnectionService.updateAccountUi();
185 }
186
187 /**
188 * Get the local client's identity key pair.
189 *
190 * @return The local client's persistent identity key pair.
191 */
192 @Override
193 public IdentityKeyPair getIdentityKeyPair() {
194 if(identityKeyPair == null) {
195 identityKeyPair = loadIdentityKeyPair();
196 }
197 return identityKeyPair;
198 }
199
200 /**
201 * Return the local client's registration ID.
202 * <p/>
203 * Clients should maintain a registration ID, a random number
204 * between 1 and 16380 that's generated once at install time.
205 *
206 * @return the local client's registration ID.
207 */
208 @Override
209 public int getLocalRegistrationId() {
210 return localRegistrationId;
211 }
212
213 /**
214 * Save a remote client's identity key
215 * <p/>
216 * Store a remote client's identity key as trusted.
217 *
218 * @param name The name of the remote client.
219 * @param identityKey The remote client's identity key.
220 */
221 @Override
222 public void saveIdentity(String name, IdentityKey identityKey) {
223 if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
224 mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
225 }
226 }
227
228 /**
229 * Verify a remote client's identity key.
230 * <p/>
231 * Determine whether a remote client's identity is trusted. Convention is
232 * that the TextSecure protocol is 'trust on first use.' This means that
233 * an identity key is considered 'trusted' if there is no entry for the recipient
234 * in the local store, or if it matches the saved key for a recipient in the local
235 * store. Only if it mismatches an entry in the local store is it considered
236 * 'untrusted.'
237 *
238 * @param name The name of the remote client.
239 * @param identityKey The identity key to verify.
240 * @return true if trusted, false if untrusted.
241 */
242 @Override
243 public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
244 //Set<IdentityKey> trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name);
245 //return trustedKeys.isEmpty() || trustedKeys.contains(identityKey);
246 return true;
247 }
248
249 // --------------------------------------
250 // SessionStore
251 // --------------------------------------
252
253 /**
254 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
255 * or a new SessionRecord if one does not currently exist.
256 * <p/>
257 * It is important that implementations return a copy of the current durable information. The
258 * returned SessionRecord may be modified, but those changes should not have an effect on the
259 * durable session state (what is returned by subsequent calls to this method) without the
260 * store method being called here first.
261 *
262 * @param address The name and device ID of the remote client.
263 * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
264 * a new SessionRecord if one does not currently exist.
265 */
266 @Override
267 public SessionRecord loadSession(AxolotlAddress address) {
268 SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
269 return (session != null) ? session : new SessionRecord();
270 }
271
272 /**
273 * Returns all known devices with active sessions for a recipient
274 *
275 * @param name the name of the client.
276 * @return all known sub-devices with active sessions.
277 */
278 @Override
279 public List<Integer> getSubDeviceSessions(String name) {
280 return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
281 new AxolotlAddress(name, 0));
282 }
283
284 /**
285 * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
286 *
287 * @param address the address of the remote client.
288 * @param record the current SessionRecord for the remote client.
289 */
290 @Override
291 public void storeSession(AxolotlAddress address, SessionRecord record) {
292 mXmppConnectionService.databaseBackend.storeSession(account, address, record);
293 }
294
295 /**
296 * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
297 *
298 * @param address the address of the remote client.
299 * @return true if a {@link SessionRecord} exists, false otherwise.
300 */
301 @Override
302 public boolean containsSession(AxolotlAddress address) {
303 return mXmppConnectionService.databaseBackend.containsSession(account, address);
304 }
305
306 /**
307 * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
308 *
309 * @param address the address of the remote client.
310 */
311 @Override
312 public void deleteSession(AxolotlAddress address) {
313 mXmppConnectionService.databaseBackend.deleteSession(account, address);
314 }
315
316 /**
317 * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
318 *
319 * @param name the name of the remote client.
320 */
321 @Override
322 public void deleteAllSessions(String name) {
323 mXmppConnectionService.databaseBackend.deleteAllSessions(account,
324 new AxolotlAddress(name, 0));
325 }
326
327 public boolean isTrustedSession(AxolotlAddress address) {
328 return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address);
329 }
330
331 public void setTrustedSession(AxolotlAddress address, boolean trusted) {
332 mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address, trusted);
333 }
334
335 // --------------------------------------
336 // PreKeyStore
337 // --------------------------------------
338
339 /**
340 * Load a local PreKeyRecord.
341 *
342 * @param preKeyId the ID of the local PreKeyRecord.
343 * @return the corresponding PreKeyRecord.
344 * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
345 */
346 @Override
347 public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
348 PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
349 if (record == null) {
350 throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
351 }
352 return record;
353 }
354
355 /**
356 * Store a local PreKeyRecord.
357 *
358 * @param preKeyId the ID of the PreKeyRecord to store.
359 * @param record the PreKeyRecord.
360 */
361 @Override
362 public void storePreKey(int preKeyId, PreKeyRecord record) {
363 mXmppConnectionService.databaseBackend.storePreKey(account, record);
364 currentPreKeyId = preKeyId;
365 boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
366 if (success) {
367 mXmppConnectionService.databaseBackend.updateAccount(account);
368 } else {
369 Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!");
370 }
371 }
372
373 /**
374 * @param preKeyId A PreKeyRecord ID.
375 * @return true if the store has a record for the preKeyId, otherwise false.
376 */
377 @Override
378 public boolean containsPreKey(int preKeyId) {
379 return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
380 }
381
382 /**
383 * Delete a PreKeyRecord from local storage.
384 *
385 * @param preKeyId The ID of the PreKeyRecord to remove.
386 */
387 @Override
388 public void removePreKey(int preKeyId) {
389 mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
390 }
391
392 // --------------------------------------
393 // SignedPreKeyStore
394 // --------------------------------------
395
396 /**
397 * Load a local SignedPreKeyRecord.
398 *
399 * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
400 * @return the corresponding SignedPreKeyRecord.
401 * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
402 */
403 @Override
404 public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
405 SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
406 if (record == null) {
407 throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
408 }
409 return record;
410 }
411
412 /**
413 * Load all local SignedPreKeyRecords.
414 *
415 * @return All stored SignedPreKeyRecords.
416 */
417 @Override
418 public List<SignedPreKeyRecord> loadSignedPreKeys() {
419 return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
420 }
421
422 /**
423 * Store a local SignedPreKeyRecord.
424 *
425 * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
426 * @param record the SignedPreKeyRecord.
427 */
428 @Override
429 public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
430 mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
431 }
432
433 /**
434 * @param signedPreKeyId A SignedPreKeyRecord ID.
435 * @return true if the store has a record for the signedPreKeyId, otherwise false.
436 */
437 @Override
438 public boolean containsSignedPreKey(int signedPreKeyId) {
439 return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
440 }
441
442 /**
443 * Delete a SignedPreKeyRecord from local storage.
444 *
445 * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
446 */
447 @Override
448 public void removeSignedPreKey(int signedPreKeyId) {
449 mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
450 }
451 }
452
453 public static class XmppAxolotlSession {
454 private SessionCipher cipher;
455 private boolean isTrusted = false;
456 private Integer preKeyId = null;
457 private SQLiteAxolotlStore sqLiteAxolotlStore;
458 private AxolotlAddress remoteAddress;
459
460 public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
461 this.cipher = new SessionCipher(store, remoteAddress);
462 this.remoteAddress = remoteAddress;
463 this.sqLiteAxolotlStore = store;
464 this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress);
465 }
466
467 public void trust() {
468 sqLiteAxolotlStore.setTrustedSession(remoteAddress, true);
469 this.isTrusted = true;
470 }
471
472 public boolean isTrusted() {
473 return this.isTrusted;
474 }
475
476 public Integer getPreKeyId() {
477 return preKeyId;
478 }
479
480 public void resetPreKeyId() {
481 preKeyId = null;
482 }
483
484 public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) {
485 byte[] plaintext = null;
486 try {
487 try {
488 PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents());
489 Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
490 plaintext = cipher.decrypt(message);
491 if (message.getPreKeyId().isPresent()) {
492 preKeyId = message.getPreKeyId().get();
493 }
494 } catch (InvalidMessageException | InvalidVersionException e) {
495 WhisperMessage message = new WhisperMessage(incomingHeader.getContents());
496 plaintext = cipher.decrypt(message);
497 } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
498 Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
499 }
500 } catch (LegacyMessageException | InvalidMessageException e) {
501 Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
502 } catch (DuplicateMessageException | NoSessionException e) {
503 Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
504 }
505 return plaintext;
506 }
507
508 public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) {
509 CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
510 XmppAxolotlMessage.XmppAxolotlMessageHeader header =
511 new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(),
512 ciphertextMessage.serialize());
513 return header;
514 }
515 }
516
517 private static class AxolotlAddressMap<T> {
518 protected Map<String, Map<Integer, T>> map;
519 protected final Object MAP_LOCK = new Object();
520
521 public AxolotlAddressMap() {
522 this.map = new HashMap<>();
523 }
524
525 public void put(AxolotlAddress address, T value) {
526 synchronized (MAP_LOCK) {
527 Map<Integer, T> devices = map.get(address.getName());
528 if (devices == null) {
529 devices = new HashMap<>();
530 map.put(address.getName(), devices);
531 }
532 devices.put(address.getDeviceId(), value);
533 }
534 }
535
536 public T get(AxolotlAddress address) {
537 synchronized (MAP_LOCK) {
538 Map<Integer, T> devices = map.get(address.getName());
539 if (devices == null) {
540 return null;
541 }
542 return devices.get(address.getDeviceId());
543 }
544 }
545
546 public Map<Integer, T> getAll(AxolotlAddress address) {
547 synchronized (MAP_LOCK) {
548 Map<Integer, T> devices = map.get(address.getName());
549 if (devices == null) {
550 return new HashMap<>();
551 }
552 return devices;
553 }
554 }
555
556 public boolean hasAny(AxolotlAddress address) {
557 synchronized (MAP_LOCK) {
558 Map<Integer, T> devices = map.get(address.getName());
559 return devices != null && !devices.isEmpty();
560 }
561 }
562
563
564 }
565
566 private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
567
568 public SessionMap(SQLiteAxolotlStore store, Account account) {
569 super();
570 this.fillMap(store, account);
571 }
572
573 private void fillMap(SQLiteAxolotlStore store, Account account) {
574 for (Contact contact : account.getRoster().getContacts()) {
575 Jid bareJid = contact.getJid().toBareJid();
576 if (bareJid == null) {
577 continue; // FIXME: handle this?
578 }
579 String address = bareJid.toString();
580 List<Integer> deviceIDs = store.getSubDeviceSessions(address);
581 for (Integer deviceId : deviceIDs) {
582 AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId);
583 this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress));
584 }
585 }
586 }
587
588 }
589
590 private static enum FetchStatus {
591 PENDING,
592 SUCCESS,
593 ERROR
594 }
595
596 private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
597
598 }
599
600 public AxolotlService(Account account, XmppConnectionService connectionService) {
601 this.mXmppConnectionService = connectionService;
602 this.account = account;
603 this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
604 this.deviceIds = new HashMap<>();
605 this.messageCache = new HashMap<>();
606 this.sessions = new SessionMap(axolotlStore, account);
607 this.fetchStatusMap = new FetchStatusMap();
608 this.executor = new SerialSingleThreadExecutor();
609 this.ownDeviceId = axolotlStore.getLocalRegistrationId();
610 }
611
612 public IdentityKey getOwnPublicKey() {
613 return axolotlStore.getIdentityKeyPair().getPublicKey();
614 }
615
616 public void trustSession(AxolotlAddress counterpart) {
617 XmppAxolotlSession session = sessions.get(counterpart);
618 if (session != null) {
619 session.trust();
620 }
621 }
622
623 public boolean isTrustedSession(AxolotlAddress counterpart) {
624 XmppAxolotlSession session = sessions.get(counterpart);
625 return session != null && session.isTrusted();
626 }
627
628 private AxolotlAddress getAddressForJid(Jid jid) {
629 return new AxolotlAddress(jid.toString(), 0);
630 }
631
632 private Set<XmppAxolotlSession> findOwnSessions() {
633 AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
634 Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
635 return ownDeviceSessions;
636 }
637
638 private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
639 AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
640 Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
641 return sessions;
642 }
643
644 private boolean hasAny(Contact contact) {
645 AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
646 return sessions.hasAny(contactAddress);
647 }
648
649 public void regenerateKeys() {
650 axolotlStore.regenerate();
651 publishBundlesIfNeeded();
652 }
653
654 public int getOwnDeviceId() {
655 return ownDeviceId;
656 }
657
658 public Set<Integer> getOwnDeviceIds() {
659 return this.deviceIds.get(account.getJid().toBareJid());
660 }
661
662 public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
663 if(deviceIds.contains(getOwnDeviceId())) {
664 Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId());
665 deviceIds.remove(getOwnDeviceId());
666 }
667 for(Integer i:deviceIds) {
668 Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i);
669 }
670 this.deviceIds.put(jid, deviceIds);
671 publishOwnDeviceIdIfNeeded();
672 }
673
674 public void wipeOtherPepDevices() {
675 Set<Integer> deviceIds = new HashSet<>();
676 deviceIds.add(getOwnDeviceId());
677 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
678 Log.d(Config.LOGTAG, "Wiping all other devices from Pep:" + publish);
679 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
680 @Override
681 public void onIqPacketReceived(Account account, IqPacket packet) {
682 // TODO: implement this!
683 }
684 });
685 }
686
687 public void publishOwnDeviceIdIfNeeded() {
688 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
689 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
690 @Override
691 public void onIqPacketReceived(Account account, IqPacket packet) {
692 Element item = mXmppConnectionService.getIqParser().getItem(packet);
693 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
694 if (deviceIds == null) {
695 deviceIds = new HashSet<Integer>();
696 }
697 if (!deviceIds.contains(getOwnDeviceId())) {
698 deviceIds.add(getOwnDeviceId());
699 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
700 Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish);
701 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
702 @Override
703 public void onIqPacketReceived(Account account, IqPacket packet) {
704 // TODO: implement this!
705 }
706 });
707 }
708 }
709 });
710 }
711
712 public void publishBundlesIfNeeded() {
713 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId);
714 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
715 @Override
716 public void onIqPacketReceived(Account account, IqPacket packet) {
717 PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
718 Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
719 boolean flush = false;
720 if (bundle == null) {
721 Log.e(Config.LOGTAG, "Received invalid bundle:" + packet);
722 bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null);
723 flush = true;
724 }
725 if (keys == null) {
726 Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet);
727 }
728 try {
729 boolean changed = false;
730 // Validate IdentityKey
731 IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
732 if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
733 Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
734 changed = true;
735 }
736
737 // Validate signedPreKeyRecord + ID
738 SignedPreKeyRecord signedPreKeyRecord;
739 int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
740 try {
741 signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
742 if ( flush
743 ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
744 || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
745 Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
746 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
747 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
748 changed = true;
749 }
750 } catch (InvalidKeyIdException e) {
751 Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
752 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
753 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
754 changed = true;
755 }
756
757 // Validate PreKeys
758 Set<PreKeyRecord> preKeyRecords = new HashSet<>();
759 if (keys != null) {
760 for (Integer id : keys.keySet()) {
761 try {
762 PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
763 if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
764 preKeyRecords.add(preKeyRecord);
765 }
766 } catch (InvalidKeyIdException ignored) {
767 }
768 }
769 }
770 int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
771 if (newKeys > 0) {
772 List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
773 axolotlStore.getCurrentPreKeyId()+1, newKeys);
774 preKeyRecords.addAll(newRecords);
775 for (PreKeyRecord record : newRecords) {
776 axolotlStore.storePreKey(record.getId(), record);
777 }
778 changed = true;
779 Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP.");
780 }
781
782
783 if(changed) {
784 IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
785 signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
786 preKeyRecords, ownDeviceId);
787 Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
788 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
789 @Override
790 public void onIqPacketReceived(Account account, IqPacket packet) {
791 // TODO: implement this!
792 Log.d(Config.LOGTAG, "Published bundle, got: " + packet);
793 }
794 });
795 }
796 } catch (InvalidKeyException e) {
797 Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
798 return;
799 }
800 }
801 });
802 }
803
804 public boolean isContactAxolotlCapable(Contact contact) {
805 Jid jid = contact.getJid().toBareJid();
806 AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
807 return sessions.hasAny(address) ||
808 ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
809 }
810
811 private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) {
812 Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId());
813
814 try {
815 IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
816 Jid.fromString(address.getName()), address.getDeviceId());
817 Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket);
818 mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
819 @Override
820 public void onIqPacketReceived(Account account, IqPacket packet) {
821 Log.d(Config.LOGTAG, "Received preKey IQ packet, processing...");
822 final IqParser parser = mXmppConnectionService.getIqParser();
823 final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
824 final PreKeyBundle bundle = parser.bundle(packet);
825 if (preKeyBundleList.isEmpty() || bundle == null) {
826 Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet);
827 fetchStatusMap.put(address, FetchStatus.ERROR);
828 return;
829 }
830 Random random = new Random();
831 final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
832 if (preKey == null) {
833 //should never happen
834 fetchStatusMap.put(address, FetchStatus.ERROR);
835 return;
836 }
837
838 final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
839 preKey.getPreKeyId(), preKey.getPreKey(),
840 bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
841 bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
842
843 axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
844
845 try {
846 SessionBuilder builder = new SessionBuilder(axolotlStore, address);
847 builder.process(preKeyBundle);
848 XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address);
849 sessions.put(address, session);
850 fetchStatusMap.put(address, FetchStatus.SUCCESS);
851 } catch (UntrustedIdentityException|InvalidKeyException e) {
852 Log.d(Config.LOGTAG, "Error building session for " + address + ": "
853 + e.getClass().getName() + ", " + e.getMessage());
854 fetchStatusMap.put(address, FetchStatus.ERROR);
855 }
856
857 AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0);
858 AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0);
859 if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
860 && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
861 conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL,
862 new Conversation.OnMessageFound() {
863 @Override
864 public void onMessageFound(Message message) {
865 processSending(message);
866 }
867 });
868 }
869 }
870 });
871 } catch (InvalidJidException e) {
872 Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName());
873 }
874 }
875
876 private boolean createSessionsIfNeeded(Conversation conversation) {
877 boolean newSessions = false;
878 Log.d(Config.LOGTAG, "Creating axolotl sessions if needed...");
879 Jid contactJid = conversation.getContact().getJid().toBareJid();
880 Set<AxolotlAddress> addresses = new HashSet<>();
881 if(deviceIds.get(contactJid) != null) {
882 for(Integer foreignId:this.deviceIds.get(contactJid)) {
883 Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId);
884 addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
885 }
886 } else {
887 Log.e(Config.LOGTAG, "Have no target devices in PEP!");
888 }
889 Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid());
890 if(deviceIds.get(account.getJid().toBareJid()) != null) {
891 for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) {
892 Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId);
893 addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
894 }
895 }
896 for (AxolotlAddress address : addresses) {
897 Log.d(Config.LOGTAG, "Processing device: " + address.toString());
898 FetchStatus status = fetchStatusMap.get(address);
899 XmppAxolotlSession session = sessions.get(address);
900 if ( session == null && ( status == null || status == FetchStatus.ERROR) ) {
901 fetchStatusMap.put(address, FetchStatus.PENDING);
902 this.buildSessionFromPEP(conversation, address);
903 newSessions = true;
904 } else {
905 Log.d(Config.LOGTAG, "Already have session for " + address.toString());
906 }
907 }
908 return newSessions;
909 }
910
911 @Nullable
912 public XmppAxolotlMessage encrypt(Message message ){
913 final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
914 ownDeviceId, message.getBody());
915
916 if(findSessionsforContact(message.getContact()).isEmpty()) {
917 return null;
918 }
919 Log.d(Config.LOGTAG, "Building axolotl foreign headers...");
920 for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
921 Log.d(Config.LOGTAG, session.remoteAddress.toString());
922 //if(!session.isTrusted()) {
923 // TODO: handle this properly
924 // continue;
925 // }
926 axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
927 }
928 Log.d(Config.LOGTAG, "Building axolotl own headers...");
929 for (XmppAxolotlSession session : findOwnSessions()) {
930 Log.d(Config.LOGTAG, session.remoteAddress.toString());
931 // if(!session.isTrusted()) {
932 // TODO: handle this properly
933 // continue;
934 // }
935 axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
936 }
937
938 return axolotlMessage;
939 }
940
941 private void processSending(final Message message) {
942 executor.execute(new Runnable() {
943 @Override
944 public void run() {
945 MessagePacket packet = mXmppConnectionService.getMessageGenerator()
946 .generateAxolotlChat(message);
947 if (packet == null) {
948 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
949 //mXmppConnectionService.updateConversationUi();
950 } else {
951 Log.d(Config.LOGTAG, "Generated message, caching: " + message.getUuid());
952 messageCache.put(message.getUuid(), packet);
953 mXmppConnectionService.resendMessage(message);
954 }
955 }
956 });
957 }
958
959 public void prepareMessage(Message message) {
960 if (!messageCache.containsKey(message.getUuid())) {
961 boolean newSessions = createSessionsIfNeeded(message.getConversation());
962
963 if (!newSessions) {
964 this.processSending(message);
965 }
966 }
967 }
968
969 public MessagePacket fetchPacketFromCache(Message message) {
970 MessagePacket packet = messageCache.get(message.getUuid());
971 if (packet != null) {
972 Log.d(Config.LOGTAG, "Cache hit: " + message.getUuid());
973 messageCache.remove(message.getUuid());
974 } else {
975 Log.d(Config.LOGTAG, "Cache miss: " + message.getUuid());
976 }
977 return packet;
978 }
979
980 public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) {
981 XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
982 AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
983 message.getSenderDeviceId());
984
985 boolean newSession = false;
986 XmppAxolotlSession session = sessions.get(senderAddress);
987 if (session == null) {
988 Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message);
989 // TODO: handle this properly
990 session = new XmppAxolotlSession(axolotlStore, senderAddress);
991 newSession = true;
992 }
993
994 for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
995 if (header.getRecipientDeviceId() == ownDeviceId) {
996 Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing...");
997 byte[] payloadKey = session.processReceiving(header);
998 if (payloadKey != null) {
999 Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message...");
1000 plaintextMessage = message.decrypt(session, payloadKey);
1001 }
1002 Integer preKeyId = session.getPreKeyId();
1003 if (preKeyId != null) {
1004 publishBundlesIfNeeded();
1005 session.resetPreKeyId();
1006 }
1007 break;
1008 }
1009 }
1010
1011 if (newSession && plaintextMessage != null) {
1012 sessions.put(senderAddress,session);
1013 }
1014
1015 return plaintextMessage;
1016 }
1017}