AxolotlService.java

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