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