001/**
002 *
003 * Copyright 2017 Paul Schaub, 2019 Florian Schmaus.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.omemo;
018
019import java.io.DataInputStream;
020import java.io.DataOutputStream;
021import java.io.EOFException;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.util.Collections;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Set;
031import java.util.SortedSet;
032import java.util.Stack;
033import java.util.TreeMap;
034import java.util.TreeSet;
035import java.util.logging.Level;
036import java.util.logging.Logger;
037
038import org.jivesoftware.smack.util.stringencoder.BareJidEncoder;
039
040import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
041import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
042import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
043
044import org.jxmpp.jid.BareJid;
045
046/**
047 * Like a rocket!
048 * Implementation of the {@link OmemoStore} class that uses plain files for storage.
049 *
050 * @author Paul Schaub
051 */
052public abstract class FileBasedOmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
053        extends OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
054
055    private final FileHierarchy hierarchy;
056    private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getName());
057    private static BareJidEncoder bareJidEncoder = new BareJidEncoder.UrlSafeEncoder();
058
059    public FileBasedOmemoStore(File basePath) {
060        super();
061        if (basePath == null) {
062            throw new IllegalStateException("No FileBasedOmemoStoreDefaultPath set in OmemoConfiguration.");
063        }
064        this.hierarchy = new FileHierarchy(basePath);
065    }
066
067    @Override
068    public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoDevice userDevice)
069            throws CorruptedOmemoKeyException, IOException {
070        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
071        return keyUtil().identityKeyPairFromBytes(readBytes(identityKeyPairPath));
072    }
073
074    @Override
075    public void storeOmemoIdentityKeyPair(OmemoDevice userDevice, T_IdKeyPair identityKeyPair) throws IOException {
076        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
077        writeBytes(identityKeyPairPath, keyUtil().identityKeyPairToBytes(identityKeyPair));
078    }
079
080    @Override
081    public void removeOmemoIdentityKeyPair(OmemoDevice userDevice) {
082        File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(userDevice);
083        if (!identityKeyPairPath.delete()) {
084            LOGGER.log(Level.WARNING, "Could not delete OMEMO IdentityKeyPair " + identityKeyPairPath.getAbsolutePath());
085        }
086    }
087
088    @Override
089    public T_IdKey loadOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice)
090            throws CorruptedOmemoKeyException, IOException {
091        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
092        byte[] bytes = readBytes(identityKeyPath);
093        return bytes != null ? keyUtil().identityKeyFromBytes(bytes) : null;
094    }
095
096    @Override
097    public void storeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice, T_IdKey t_idKey) throws IOException {
098        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
099        writeBytes(identityKeyPath, keyUtil().identityKeyToBytes(t_idKey));
100    }
101
102    @Override
103    public void removeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) {
104        File identityKeyPath = hierarchy.getContactsIdentityKeyPath(userDevice, contactsDevice);
105        if (!identityKeyPath.delete()) {
106            LOGGER.log(Level.WARNING, "Could not delete OMEMO identityKey " + identityKeyPath.getAbsolutePath());
107        }
108    }
109
110    @Override
111    public SortedSet<Integer> localDeviceIdsOf(BareJid localUser) {
112        SortedSet<Integer> deviceIds = new TreeSet<>();
113        File userDir = hierarchy.getUserDirectory(localUser);
114        File[] list = userDir.listFiles();
115        for (File d : list != null ? list : new File[] {}) {
116            if (d.isDirectory()) {
117                try {
118                    deviceIds.add(Integer.parseInt(d.getName()));
119                } catch (NumberFormatException e) {
120                    // ignore
121                }
122            }
123        }
124        return deviceIds;
125    }
126
127    @Override
128    public void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException {
129        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
130        writeLong(lastMessageReceived, date.getTime());
131    }
132
133    @Override
134    public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
135        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
136        Long date = readLong(lastMessageReceived);
137        return date != null ? new Date(date) : null;
138    }
139
140    @Override
141    public void setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException {
142        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
143        writeLong(lastDeviceIdPublished, date.getTime());
144    }
145
146    @Override
147    public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
148        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
149        Long date = readLong(lastDeviceIdPublished);
150        return date != null ? new Date(date) : null;
151    }
152
153    @Override
154    public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) throws IOException {
155        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
156        writeLong(lastSignedPreKeyRenewal, date.getTime());
157    }
158
159    @Override
160    public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) throws IOException {
161        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
162        Long date = readLong(lastSignedPreKeyRenewal);
163        return date != null ? new Date(date) : null;
164    }
165
166    @Override
167    public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) throws IOException {
168        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
169        byte[] bytes = readBytes(preKeyPath);
170
171        if (bytes != null) {
172            try {
173                return keyUtil().preKeyFromBytes(bytes);
174            } catch (IOException e) {
175                LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
176            }
177        }
178
179        return null;
180    }
181
182    @Override
183    public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) throws IOException {
184        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
185        writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey));
186    }
187
188    @Override
189    public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
190        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
191        if (!preKeyPath.delete()) {
192            LOGGER.log(Level.WARNING, "Deleting OMEMO preKey " + preKeyPath.getAbsolutePath() + " failed.");
193        }
194    }
195
196    @Override
197    public TreeMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoDevice userDevice) throws IOException {
198        File preKeyDirectory = hierarchy.getPreKeysDirectory(userDevice);
199        TreeMap<Integer, T_PreKey> preKeys = new TreeMap<>();
200
201        if (preKeyDirectory == null) {
202            return preKeys;
203        }
204
205        File[] keys = preKeyDirectory.listFiles();
206
207        for (File f : keys != null ? keys : new File[0]) {
208            byte[] bytes = readBytes(f);
209            if (bytes != null) {
210                try {
211                    T_PreKey p = keyUtil().preKeyFromBytes(bytes);
212                    preKeys.put(Integer.parseInt(f.getName()), p);
213                } catch (IOException e) {
214                    LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
215                }
216            }
217        }
218
219        return preKeys;
220    }
221
222    @Override
223    public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) throws IOException {
224        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
225        byte[] bytes = readBytes(signedPreKeyPath);
226        if (bytes != null) {
227            try {
228                return keyUtil().signedPreKeyFromBytes(bytes);
229            } catch (IOException e) {
230                LOGGER.log(Level.WARNING, "Could not deserialize signed preKey from bytes.", e);
231            }
232        }
233        return null;
234    }
235
236    @Override
237    public TreeMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException {
238        File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(userDevice);
239        TreeMap<Integer, T_SigPreKey> signedPreKeys = new TreeMap<>();
240
241        if (signedPreKeysDirectory == null) {
242            return signedPreKeys;
243        }
244
245        File[] keys = signedPreKeysDirectory.listFiles();
246
247        for (File f : keys != null ? keys : new File[0]) {
248            byte[] bytes = readBytes(f);
249            if (bytes != null) {
250                try {
251                    T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes);
252                    signedPreKeys.put(Integer.parseInt(f.getName()), p);
253                } catch (IOException e) {
254                    LOGGER.log(Level.WARNING, "Could not deserialize signed preKey.", e);
255                }
256            }
257        }
258
259        return signedPreKeys;
260    }
261
262    @Override
263    public void storeOmemoSignedPreKey(OmemoDevice userDevice,
264                                       int signedPreKeyId,
265                                       T_SigPreKey signedPreKey) throws IOException {
266        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
267        writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey));
268    }
269
270    @Override
271    public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
272        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
273        if (!signedPreKeyPath.delete()) {
274            LOGGER.log(Level.WARNING, "Deleting signed OMEMO preKey " + signedPreKeyPath.getAbsolutePath() + " failed.");
275        }
276    }
277
278    @Override
279    public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
280        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
281        byte[] bytes = readBytes(sessionPath);
282        if (bytes != null) {
283            try {
284                return keyUtil().rawSessionFromBytes(bytes);
285            } catch (IOException e) {
286                LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
287            }
288        }
289        return null;
290    }
291
292    @Override
293    public HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException {
294        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
295        HashMap<Integer, T_Sess> sessions = new HashMap<>();
296        String[] devices = contactsDirectory.list();
297
298        for (String deviceId : devices != null ? devices : new String[0]) {
299            int id;
300            try {
301                id = Integer.parseInt(deviceId);
302            } catch (NumberFormatException e) {
303                continue;
304            }
305            OmemoDevice device = new OmemoDevice(contact, id);
306            File session = hierarchy.getContactsSessionPath(userDevice, device);
307
308            byte[] bytes = readBytes(session);
309
310            if (bytes != null) {
311                try {
312                    T_Sess s = keyUtil().rawSessionFromBytes(bytes);
313                    sessions.put(id, s);
314                } catch (IOException e) {
315                    LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
316                }
317            }
318
319        }
320        return sessions;
321    }
322
323    @Override
324    public void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice, T_Sess session) throws IOException {
325        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
326        writeBytes(sessionPath, keyUtil().rawSessionToBytes(session));
327    }
328
329    @Override
330    public void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
331        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
332        if (!sessionPath.delete()) {
333            LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + sessionPath.getAbsolutePath() + " failed.");
334        }
335    }
336
337    @Override
338    public void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
339        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
340        String[] devices = contactsDirectory.list();
341
342        for (String deviceId : devices != null ? devices : new String[0]) {
343            int id = Integer.parseInt(deviceId);
344            OmemoDevice device = new OmemoDevice(contact, id);
345            File session = hierarchy.getContactsSessionPath(userDevice, device);
346            if (!session.delete()) {
347                LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + session.getAbsolutePath() + "failed.");
348            }
349        }
350    }
351
352    @Override
353    public boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
354        File session = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
355        return session.exists();
356    }
357
358    @Override
359    public void storeOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice, int counter) throws IOException {
360        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
361        writeIntegers(messageCounterFile, Collections.singleton(counter));
362    }
363
364    @Override
365    public int loadOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
366        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
367        Set<Integer> integers = readIntegers(messageCounterFile);
368
369        if (integers == null || integers.isEmpty()) {
370            return 0;
371        }
372
373        return integers.iterator().next();
374    }
375
376    @Override
377    public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) throws IOException {
378        OmemoCachedDeviceList cachedDeviceList = new OmemoCachedDeviceList();
379
380        if (contact == null) {
381            throw new IllegalArgumentException("Contact can not be null.");
382        }
383
384        // active
385        File activeDevicesPath = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
386        Set<Integer> active = readIntegers(activeDevicesPath);
387        if (active != null) {
388            cachedDeviceList.getActiveDevices().addAll(active);
389        }
390
391        // inactive
392        File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
393        Set<Integer> inactive = readIntegers(inactiveDevicesPath);
394        if (inactive != null) {
395            cachedDeviceList.getInactiveDevices().addAll(inactive);
396        }
397
398        return cachedDeviceList;
399    }
400
401    @Override
402    public void storeCachedDeviceList(OmemoDevice userDevice,
403                                      BareJid contact,
404                                      OmemoCachedDeviceList contactsDeviceList) throws IOException {
405        if (contact == null) {
406            return;
407        }
408
409        File activeDevices = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
410        writeIntegers(activeDevices, contactsDeviceList.getActiveDevices());
411
412        File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
413        writeIntegers(inactiveDevices, contactsDeviceList.getInactiveDevices());
414    }
415
416    @Override
417    public void purgeOwnDeviceKeys(OmemoDevice userDevice) {
418        File deviceDirectory = hierarchy.getUserDeviceDirectory(userDevice);
419        deleteDirectory(deviceDirectory);
420    }
421
422    private static void writeLong(File target, long i) throws IOException {
423        if (target == null) {
424            throw new IOException("Could not write long to null-path.");
425        }
426
427        FileHierarchy.createFile(target);
428
429        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
430            out.writeLong(i);
431        }
432    }
433
434    private static Long readLong(File target) throws IOException {
435        if (target == null) {
436            throw new IOException("Could not read long from null-path.");
437        }
438
439        if (!target.exists() || !target.isFile()) {
440            return null;
441        }
442
443        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
444            return in.readLong();
445        }
446    }
447
448    private static void writeBytes(File target, byte[] bytes) throws IOException {
449        if (target == null) {
450            throw new IOException("Could not write bytes to null-path.");
451        }
452
453        // Create file
454        FileHierarchy.createFile(target);
455
456        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
457            out.write(bytes);
458        }
459    }
460
461    private static byte[] readBytes(File target) throws IOException {
462        if (target == null) {
463            throw new IOException("Could not read bytes from null-path.");
464        }
465
466        if (!target.exists() || !target.isFile()) {
467            return null;
468        }
469
470        byte[] b = new byte[(int) target.length()];
471        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
472            in.read(b);
473        }
474
475        return b;
476    }
477
478    private static void writeIntegers(File target, Set<Integer> integers) throws IOException {
479        if (target == null) {
480            throw new IOException("Could not write integers to null-path.");
481        }
482
483        FileHierarchy.createFile(target);
484
485        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
486            for (int i : integers) {
487                out.writeInt(i);
488            }
489        }
490    }
491
492    private static Set<Integer> readIntegers(File target) throws IOException {
493        if (target == null) {
494            throw new IOException("Could not write integers to null-path.");
495        }
496
497        if (!target.exists() || !target.isFile()) {
498            return null;
499        }
500
501        HashSet<Integer> integers = new HashSet<>();
502
503        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
504            while (true) {
505                try {
506                    integers.add(in.readInt());
507                } catch (EOFException e) {
508                    break;
509                }
510            }
511        }
512
513        return integers;
514    }
515
516    /**
517     * Delete a directory with all subdirectories.
518     * @param root directory to be deleted
519     */
520    public static void deleteDirectory(File root) {
521        File[] currList;
522        Stack<File> stack = new Stack<>();
523        stack.push(root);
524        while (!stack.isEmpty()) {
525            if (stack.lastElement().isDirectory()) {
526                currList = stack.lastElement().listFiles();
527                if (currList != null && currList.length > 0) {
528                    for (File curr : currList) {
529                        stack.push(curr);
530                    }
531                } else {
532                    stack.pop().delete();
533                }
534            } else {
535                stack.pop().delete();
536            }
537        }
538    }
539
540    /**
541     * This class represents the directory structure of the FileBasedOmemoStore.
542     * The directory looks as follows:
543     *
544     *  OMEMO_Store/
545     *      'romeo@montague.lit'/                           //Our bareJid
546     *          ...
547     *      'juliet@capulet.lit'/                           //Our other bareJid
548     *          '13371234'/                                 //deviceId
549     *              identityKeyPair                         //Our identityKeyPair
550     *              lastSignedPreKeyRenewal                 //Date of when the signedPreKey was last renewed.
551     *              preKeys/                                //Our preKeys
552     *                  '1'
553     *                  '2'
554     *                  ...
555     *              signedPreKeys/                          //Our signedPreKeys
556     *                  '1'
557     *                  '2'
558     *                  ...
559     *              contacts/
560     *                  'romeo@capulet.lit'/                //Juliets contact Romeo
561     *                      activeDevice                    //List of Romeos active devices
562     *                      inactiveDevices                 //List of his inactive devices
563     *                      'deviceId'/                     //Romeos deviceId
564     *                          identityKey                 //Romeos identityKey
565     *                          session                     //Our session with romeo
566     *                          trust                       //Records about the trust in romeos device
567     *                          (lastReceivedMessageDate)   //Only, for our own other devices:
568     *                                                          //date of the last received message
569     *
570     */
571    public static class FileHierarchy {
572
573        static final String STORE = "OMEMO_Store";
574        static final String CONTACTS = "contacts";
575        static final String IDENTITY_KEY = "identityKey";
576        static final String IDENTITY_KEY_PAIR = "identityKeyPair";
577        static final String PRE_KEYS = "preKeys";
578        static final String LAST_MESSAGE_RECEVIED_DATE = "lastMessageReceivedDate";
579        static final String LAST_DEVICEID_PUBLICATION_DATE = "lastDeviceIdPublicationDate";
580        static final String SIGNED_PRE_KEYS = "signedPreKeys";
581        static final String LAST_SIGNED_PRE_KEY_RENEWAL = "lastSignedPreKeyRenewal";
582        static final String SESSION = "session";
583        static final String DEVICE_LIST_ACTIVE = "activeDevices";
584        static final String DEVICE_LIST_INAVTIVE = "inactiveDevices";
585        static final String MESSAGE_COUNTER = "messageCounter";
586
587        File basePath;
588
589        FileHierarchy(File basePath) {
590            this.basePath = basePath;
591            basePath.mkdirs();
592        }
593
594        File getStoreDirectory() {
595            return createDirectory(basePath, STORE);
596        }
597
598        File getUserDirectory(OmemoDevice userDevice) {
599            return getUserDirectory(userDevice.getJid());
600        }
601
602        File getUserDirectory(BareJid bareJid) {
603            return createDirectory(getStoreDirectory(), bareJidEncoder.encode(bareJid));
604        }
605
606        File getUserDeviceDirectory(OmemoDevice userDevice) {
607            return createDirectory(getUserDirectory(userDevice.getJid()),
608                    Integer.toString(userDevice.getDeviceId()));
609        }
610
611        File getContactsDir(OmemoDevice userDevice) {
612            return createDirectory(getUserDeviceDirectory(userDevice), CONTACTS);
613        }
614
615        File getContactsDir(OmemoDevice userDevice, BareJid contact) {
616            return createDirectory(getContactsDir(userDevice), bareJidEncoder.encode(contact));
617        }
618
619        File getContactsDir(OmemoDevice userDevice, OmemoDevice contactsDevice) {
620            return createDirectory(getContactsDir(userDevice, contactsDevice.getJid()),
621                    Integer.toString(contactsDevice.getDeviceId()));
622        }
623
624        File getIdentityKeyPairPath(OmemoDevice userDevice) {
625            return new File(getUserDeviceDirectory(userDevice), IDENTITY_KEY_PAIR);
626        }
627
628        File getPreKeysDirectory(OmemoDevice userDevice) {
629            return createDirectory(getUserDeviceDirectory(userDevice), PRE_KEYS);
630        }
631
632        File getPreKeyPath(OmemoDevice userDevice, int preKeyId) {
633            return new File(getPreKeysDirectory(userDevice), Integer.toString(preKeyId));
634        }
635
636        File getLastMessageReceivedDatePath(OmemoDevice userDevice, OmemoDevice device) {
637            return new File(getContactsDir(userDevice, device), LAST_MESSAGE_RECEVIED_DATE);
638        }
639
640        File getLastDeviceIdPublicationDatePath(OmemoDevice userDevice, OmemoDevice device) {
641            return new File(getContactsDir(userDevice, device), LAST_DEVICEID_PUBLICATION_DATE);
642        }
643
644        File getSignedPreKeysDirectory(OmemoDevice userDevice) {
645            return createDirectory(getUserDeviceDirectory(userDevice), SIGNED_PRE_KEYS);
646        }
647
648        File getLastSignedPreKeyRenewal(OmemoDevice userDevice) {
649            return new File(getUserDeviceDirectory(userDevice), LAST_SIGNED_PRE_KEY_RENEWAL);
650        }
651
652        File getContactsIdentityKeyPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
653            return new File(getContactsDir(userDevice, contactsDevice), IDENTITY_KEY);
654
655        }
656
657        File getContactsSessionPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
658            return new File(getContactsDir(userDevice, contactsDevice), SESSION);
659        }
660
661        File getContactsActiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
662            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_ACTIVE);
663        }
664
665        File getContactsInactiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
666            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_INAVTIVE);
667        }
668
669        File getDevicesMessageCounterPath(OmemoDevice userDevice, OmemoDevice otherDevice) {
670            return new File(getContactsDir(userDevice, otherDevice), MESSAGE_COUNTER);
671        }
672
673        private static File createFile(File f) throws IOException {
674            File p = f.getParentFile();
675            createDirectory(p);
676            f.createNewFile();
677            return f;
678
679        }
680
681        private static File createDirectory(File dir, String subdir) {
682            File f = new File(dir, subdir);
683            return createDirectory(f);
684        }
685
686        private static File createDirectory(File f) {
687            if (f.exists() && f.isDirectory()) {
688                return f;
689            }
690
691            f.mkdirs();
692            return f;
693        }
694    }
695
696    /**
697     * Convert {@link BareJid BareJids} to Strings using the legacy {@link BareJid#toString()} method instead of the
698     * proper, url safe {@link BareJid#asUrlEncodedString()} method.
699     * While it is highly advised to use the new format, you can use this method to stay backwards compatible to data
700     * sets created by the old implementation.
701     */
702    @SuppressWarnings("deprecation")
703    public static void useLegacyBareJidEncoding() {
704        bareJidEncoder = new BareJidEncoder.LegacyEncoder();
705    }
706}