AxolotlService.java

  1package eu.siacs.conversations.crypto.axolotl;
  2
  3import android.util.Log;
  4
  5import org.whispersystems.libaxolotl.AxolotlAddress;
  6import org.whispersystems.libaxolotl.IdentityKey;
  7import org.whispersystems.libaxolotl.IdentityKeyPair;
  8import org.whispersystems.libaxolotl.InvalidKeyException;
  9import org.whispersystems.libaxolotl.InvalidKeyIdException;
 10import org.whispersystems.libaxolotl.ecc.Curve;
 11import org.whispersystems.libaxolotl.ecc.ECKeyPair;
 12import org.whispersystems.libaxolotl.state.AxolotlStore;
 13import org.whispersystems.libaxolotl.state.PreKeyRecord;
 14import org.whispersystems.libaxolotl.state.SessionRecord;
 15import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
 16import org.whispersystems.libaxolotl.util.KeyHelper;
 17
 18import java.util.ArrayList;
 19import java.util.HashMap;
 20import java.util.List;
 21import java.util.Map;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.entities.Account;
 25import eu.siacs.conversations.entities.Conversation;
 26import eu.siacs.conversations.entities.Message;
 27import eu.siacs.conversations.services.XmppConnectionService;
 28import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 29import eu.siacs.conversations.xmpp.jid.Jid;
 30
 31public class AxolotlService {
 32
 33    private Account account;
 34    private XmppConnectionService mXmppConnectionService;
 35    private SQLiteAxolotlStore axolotlStore;
 36    private Map<Jid,XmppAxolotlSession> sessions;
 37
 38    public static class SQLiteAxolotlStore implements AxolotlStore {
 39
 40        public static final String PREKEY_TABLENAME = "prekeys";
 41        public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
 42        public static final String SESSION_TABLENAME = "signed_prekeys";
 43        public static final String NAME = "name";
 44        public static final String DEVICE_ID = "device_id";
 45        public static final String ID = "id";
 46        public static final String KEY = "key";
 47        public static final String ACCOUNT = "account";
 48
 49        public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key";
 50        public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
 51
 52        private final Account account;
 53        private final XmppConnectionService mXmppConnectionService;
 54
 55        private final IdentityKeyPair identityKeyPair;
 56        private final int localRegistrationId;
 57
 58
 59        private static IdentityKeyPair generateIdentityKeyPair() {
 60            Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair...");
 61            ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
 62            IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
 63                    identityKeyPairKeys.getPrivateKey());
 64            return ownKey;
 65        }
 66
 67        private static int generateRegistrationId() {
 68            Log.d(Config.LOGTAG, "Generating axolotl registration ID...");
 69            int reg_id = KeyHelper.generateRegistrationId(false);
 70            return reg_id;
 71        }
 72
 73        public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
 74            this.account = account;
 75            this.mXmppConnectionService = service;
 76            this.identityKeyPair = loadIdentityKeyPair();
 77            this.localRegistrationId = loadRegistrationId();
 78        }
 79
 80        // --------------------------------------
 81        // IdentityKeyStore
 82        // --------------------------------------
 83
 84        private IdentityKeyPair loadIdentityKeyPair() {
 85            String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR);
 86            IdentityKeyPair ownKey;
 87            if( serializedKey != null ) {
 88                try {
 89                    ownKey = new IdentityKeyPair(serializedKey.getBytes());
 90                } catch (InvalidKeyException e) {
 91                    Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage());
 92                    return null;
 93                }
 94            } else {
 95                Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid());
 96                ownKey = generateIdentityKeyPair();
 97                boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize()));
 98                if(success) {
 99                    mXmppConnectionService.databaseBackend.updateAccount(account);
100                } else {
101                    Log.e(Config.LOGTAG, "Failed to write new key to the database!");
102                }
103            }
104            return ownKey;
105        }
106
107        private int loadRegistrationId() {
108            String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
109            int reg_id;
110            if (regIdString != null) {
111                reg_id = Integer.valueOf(regIdString);
112            } else {
113                Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid());
114                reg_id = generateRegistrationId();
115                boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id);
116                if(success) {
117                    mXmppConnectionService.databaseBackend.updateAccount(account);
118                } else {
119                    Log.e(Config.LOGTAG, "Failed to write new key to the database!");
120                }
121            }
122            return reg_id;
123        }
124
125        /**
126         * Get the local client's identity key pair.
127         *
128         * @return The local client's persistent identity key pair.
129         */
130        @Override
131        public IdentityKeyPair getIdentityKeyPair() {
132            return identityKeyPair;
133        }
134
135        /**
136         * Return the local client's registration ID.
137         * <p/>
138         * Clients should maintain a registration ID, a random number
139         * between 1 and 16380 that's generated once at install time.
140         *
141         * @return the local client's registration ID.
142         */
143        @Override
144        public int getLocalRegistrationId() {
145            return localRegistrationId;
146        }
147
148        /**
149         * Save a remote client's identity key
150         * <p/>
151         * Store a remote client's identity key as trusted.
152         *
153         * @param name        The name of the remote client.
154         * @param identityKey The remote client's identity key.
155         */
156        @Override
157        public void saveIdentity(String name, IdentityKey identityKey) {
158            try {
159                Jid contactJid = Jid.fromString(name);
160                Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
161                if (conversation != null) {
162                    conversation.getContact().addAxolotlIdentityKey(identityKey, false);
163                    mXmppConnectionService.updateConversationUi();
164                    mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
165                }
166            } catch (final InvalidJidException e) {
167                Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString());
168            }
169        }
170
171        /**
172         * Verify a remote client's identity key.
173         * <p/>
174         * Determine whether a remote client's identity is trusted.  Convention is
175         * that the TextSecure protocol is 'trust on first use.'  This means that
176         * an identity key is considered 'trusted' if there is no entry for the recipient
177         * in the local store, or if it matches the saved key for a recipient in the local
178         * store.  Only if it mismatches an entry in the local store is it considered
179         * 'untrusted.'
180         *
181         * @param name        The name of the remote client.
182         * @param identityKey The identity key to verify.
183         * @return true if trusted, false if untrusted.
184         */
185        @Override
186        public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
187            try {
188                Jid contactJid = Jid.fromString(name);
189                Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
190                if (conversation != null) {
191                    List<IdentityKey> trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys();
192                    return trustedKeys.contains(identityKey);
193                } else {
194                    return false;
195                }
196            } catch (final InvalidJidException e) {
197                Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString());
198                return false;
199            }
200        }
201
202        // --------------------------------------
203        // SessionStore
204        // --------------------------------------
205
206        /**
207         * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
208         * or a new SessionRecord if one does not currently exist.
209         * <p/>
210         * It is important that implementations return a copy of the current durable information.  The
211         * returned SessionRecord may be modified, but those changes should not have an effect on the
212         * durable session state (what is returned by subsequent calls to this method) without the
213         * store method being called here first.
214         *
215         * @param address The name and device ID of the remote client.
216         * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
217         * a new SessionRecord if one does not currently exist.
218         */
219        @Override
220        public SessionRecord loadSession(AxolotlAddress address) {
221            SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
222            return (session!=null)?session:new SessionRecord();
223        }
224
225        /**
226         * Returns all known devices with active sessions for a recipient
227         *
228         * @param name the name of the client.
229         * @return all known sub-devices with active sessions.
230         */
231        @Override
232        public List<Integer> getSubDeviceSessions(String name) {
233            return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
234                    new AxolotlAddress(name,0));
235        }
236
237        /**
238         * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
239         *
240         * @param address the address of the remote client.
241         * @param record  the current SessionRecord for the remote client.
242         */
243        @Override
244        public void storeSession(AxolotlAddress address, SessionRecord record) {
245            mXmppConnectionService.databaseBackend.storeSession(account, address, record);
246        }
247
248        /**
249         * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
250         *
251         * @param address the address of the remote client.
252         * @return true if a {@link SessionRecord} exists, false otherwise.
253         */
254        @Override
255        public boolean containsSession(AxolotlAddress address) {
256            return mXmppConnectionService.databaseBackend.containsSession(account, address);
257        }
258
259        /**
260         * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
261         *
262         * @param address the address of the remote client.
263         */
264        @Override
265        public void deleteSession(AxolotlAddress address) {
266            mXmppConnectionService.databaseBackend.deleteSession(account, address);
267        }
268
269        /**
270         * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
271         *
272         * @param name the name of the remote client.
273         */
274        @Override
275        public void deleteAllSessions(String name) {
276            mXmppConnectionService.databaseBackend.deleteAllSessions(account,
277                    new AxolotlAddress(name,0));
278        }
279
280        // --------------------------------------
281        // PreKeyStore
282        // --------------------------------------
283
284        /**
285         * Load a local PreKeyRecord.
286         *
287         * @param preKeyId the ID of the local PreKeyRecord.
288         * @return the corresponding PreKeyRecord.
289         * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
290         */
291        @Override
292        public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
293            PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
294            if(record == null) {
295                throw new InvalidKeyIdException("No such PreKeyRecord!");
296            }
297            return record;
298        }
299
300        /**
301         * Store a local PreKeyRecord.
302         *
303         * @param preKeyId the ID of the PreKeyRecord to store.
304         * @param record   the PreKeyRecord.
305         */
306        @Override
307        public void storePreKey(int preKeyId, PreKeyRecord record) {
308            mXmppConnectionService.databaseBackend.storePreKey(account, record);
309        }
310
311        /**
312         * @param preKeyId A PreKeyRecord ID.
313         * @return true if the store has a record for the preKeyId, otherwise false.
314         */
315        @Override
316        public boolean containsPreKey(int preKeyId) {
317            return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
318        }
319
320        /**
321         * Delete a PreKeyRecord from local storage.
322         *
323         * @param preKeyId The ID of the PreKeyRecord to remove.
324         */
325        @Override
326        public void removePreKey(int preKeyId) {
327            mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
328        }
329
330        // --------------------------------------
331        // SignedPreKeyStore
332        // --------------------------------------
333
334        /**
335         * Load a local SignedPreKeyRecord.
336         *
337         * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
338         * @return the corresponding SignedPreKeyRecord.
339         * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
340         */
341        @Override
342        public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
343            SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
344            if(record == null) {
345                throw new InvalidKeyIdException("No such PreKeyRecord!");
346            }
347            return record;
348        }
349
350        /**
351         * Load all local SignedPreKeyRecords.
352         *
353         * @return All stored SignedPreKeyRecords.
354         */
355        @Override
356        public List<SignedPreKeyRecord> loadSignedPreKeys() {
357            return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
358        }
359
360        /**
361         * Store a local SignedPreKeyRecord.
362         *
363         * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
364         * @param record         the SignedPreKeyRecord.
365         */
366        @Override
367        public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
368            mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
369        }
370
371        /**
372         * @param signedPreKeyId A SignedPreKeyRecord ID.
373         * @return true if the store has a record for the signedPreKeyId, otherwise false.
374         */
375        @Override
376        public boolean containsSignedPreKey(int signedPreKeyId) {
377            return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
378        }
379
380        /**
381         * Delete a SignedPreKeyRecord from local storage.
382         *
383         * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
384         */
385        @Override
386        public void removeSignedPreKey(int signedPreKeyId) {
387            mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
388        }
389    }
390
391    private static class XmppAxolotlSession {
392        private List<Message> untrustedMessages;
393        private AxolotlStore axolotlStore;
394
395        public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) {
396            this.untrustedMessages = new ArrayList<>();
397            this.axolotlStore = axolotlStore;
398        }
399
400        public void trust() {
401            for (Message message : this.untrustedMessages) {
402                message.trust();
403            }
404            this.untrustedMessages = null;
405        }
406
407        public boolean isTrusted() {
408            return (this.untrustedMessages == null);
409        }
410
411        public String processReceiving(XmppAxolotlMessage incomingMessage) {
412            return null;
413        }
414
415        public XmppAxolotlMessage processSending(String outgoingMessage) {
416            return null;
417        }
418    }
419
420    public AxolotlService(Account account, XmppConnectionService connectionService) {
421        this.mXmppConnectionService = connectionService;
422        this.account = account;
423        this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
424        this.sessions = new HashMap<>();
425    }
426
427    public void trustSession(Jid counterpart) {
428        XmppAxolotlSession session = sessions.get(counterpart);
429        if(session != null) {
430            session.trust();
431        }
432    }
433
434    public boolean isTrustedSession(Jid counterpart) {
435        XmppAxolotlSession session = sessions.get(counterpart);
436        return session != null && session.isTrusted();
437    }
438
439
440}