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