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    @SuppressWarnings("JavaUtilDate")
129    public void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException {
130        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
131        writeLong(lastMessageReceived, date.getTime());
132    }
133
134    @Override
135    @SuppressWarnings("JavaUtilDate")
136    public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
137        File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice);
138        Long date = readLong(lastMessageReceived);
139        return date != null ? new Date(date) : null;
140    }
141
142    @Override
143    @SuppressWarnings("JavaUtilDate")
144    public void setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException {
145        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
146        writeLong(lastDeviceIdPublished, date.getTime());
147    }
148
149    @Override
150    @SuppressWarnings("JavaUtilDate")
151    public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
152        File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice);
153        Long date = readLong(lastDeviceIdPublished);
154        return date != null ? new Date(date) : null;
155    }
156
157    @Override
158    @SuppressWarnings("JavaUtilDate")
159    public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) throws IOException {
160        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
161        writeLong(lastSignedPreKeyRenewal, date.getTime());
162    }
163
164    @Override
165    @SuppressWarnings("JavaUtilDate")
166    public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) throws IOException {
167        File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice);
168        Long date = readLong(lastSignedPreKeyRenewal);
169        return date != null ? new Date(date) : null;
170    }
171
172    @Override
173    public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) throws IOException {
174        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
175        byte[] bytes = readBytes(preKeyPath);
176
177        if (bytes != null) {
178            try {
179                return keyUtil().preKeyFromBytes(bytes);
180            } catch (IOException e) {
181                LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
182            }
183        }
184
185        return null;
186    }
187
188    @Override
189    public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) throws IOException {
190        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
191        writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey));
192    }
193
194    @Override
195    public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
196        File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId);
197        if (!preKeyPath.delete()) {
198            LOGGER.log(Level.WARNING, "Deleting OMEMO preKey " + preKeyPath.getAbsolutePath() + " failed.");
199        }
200    }
201
202    @Override
203    @SuppressWarnings("NonApiType")
204    public TreeMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoDevice userDevice) throws IOException {
205        File preKeyDirectory = hierarchy.getPreKeysDirectory(userDevice);
206        TreeMap<Integer, T_PreKey> preKeys = new TreeMap<>();
207
208        if (preKeyDirectory == null) {
209            return preKeys;
210        }
211
212        File[] keys = preKeyDirectory.listFiles();
213
214        for (File f : keys != null ? keys : new File[0]) {
215            byte[] bytes = readBytes(f);
216            if (bytes != null) {
217                try {
218                    T_PreKey p = keyUtil().preKeyFromBytes(bytes);
219                    preKeys.put(Integer.parseInt(f.getName()), p);
220                } catch (IOException e) {
221                    LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e);
222                }
223            }
224        }
225
226        return preKeys;
227    }
228
229    @Override
230    public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) throws IOException {
231        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
232        byte[] bytes = readBytes(signedPreKeyPath);
233        if (bytes != null) {
234            try {
235                return keyUtil().signedPreKeyFromBytes(bytes);
236            } catch (IOException e) {
237                LOGGER.log(Level.WARNING, "Could not deserialize signed preKey from bytes.", e);
238            }
239        }
240        return null;
241    }
242
243    @Override
244    @SuppressWarnings("NonApiType")
245    public TreeMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException {
246        File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(userDevice);
247        TreeMap<Integer, T_SigPreKey> signedPreKeys = new TreeMap<>();
248
249        if (signedPreKeysDirectory == null) {
250            return signedPreKeys;
251        }
252
253        File[] keys = signedPreKeysDirectory.listFiles();
254
255        for (File f : keys != null ? keys : new File[0]) {
256            byte[] bytes = readBytes(f);
257            if (bytes != null) {
258                try {
259                    T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes);
260                    signedPreKeys.put(Integer.parseInt(f.getName()), p);
261                } catch (IOException e) {
262                    LOGGER.log(Level.WARNING, "Could not deserialize signed preKey.", e);
263                }
264            }
265        }
266
267        return signedPreKeys;
268    }
269
270    @Override
271    public void storeOmemoSignedPreKey(OmemoDevice userDevice,
272                                       int signedPreKeyId,
273                                       T_SigPreKey signedPreKey) throws IOException {
274        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
275        writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey));
276    }
277
278    @Override
279    public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
280        File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId));
281        if (!signedPreKeyPath.delete()) {
282            LOGGER.log(Level.WARNING, "Deleting signed OMEMO preKey " + signedPreKeyPath.getAbsolutePath() + " failed.");
283        }
284    }
285
286    @Override
287    public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
288        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
289        byte[] bytes = readBytes(sessionPath);
290        if (bytes != null) {
291            try {
292                return keyUtil().rawSessionFromBytes(bytes);
293            } catch (IOException e) {
294                LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
295            }
296        }
297        return null;
298    }
299
300    @Override
301    @SuppressWarnings("NonApiType")
302    public HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException {
303        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
304        HashMap<Integer, T_Sess> sessions = new HashMap<>();
305        String[] devices = contactsDirectory.list();
306
307        for (String deviceId : devices != null ? devices : new String[0]) {
308            int id;
309            try {
310                id = Integer.parseInt(deviceId);
311            } catch (NumberFormatException e) {
312                continue;
313            }
314            OmemoDevice device = new OmemoDevice(contact, id);
315            File session = hierarchy.getContactsSessionPath(userDevice, device);
316
317            byte[] bytes = readBytes(session);
318
319            if (bytes != null) {
320                try {
321                    T_Sess s = keyUtil().rawSessionFromBytes(bytes);
322                    sessions.put(id, s);
323                } catch (IOException e) {
324                    LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e);
325                }
326            }
327
328        }
329        return sessions;
330    }
331
332    @Override
333    public void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice, T_Sess session) throws IOException {
334        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
335        writeBytes(sessionPath, keyUtil().rawSessionToBytes(session));
336    }
337
338    @Override
339    public void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
340        File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
341        if (!sessionPath.delete()) {
342            LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + sessionPath.getAbsolutePath() + " failed.");
343        }
344    }
345
346    @Override
347    public void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
348        File contactsDirectory = hierarchy.getContactsDir(userDevice, contact);
349        String[] devices = contactsDirectory.list();
350
351        for (String deviceId : devices != null ? devices : new String[0]) {
352            int id = Integer.parseInt(deviceId);
353            OmemoDevice device = new OmemoDevice(contact, id);
354            File session = hierarchy.getContactsSessionPath(userDevice, device);
355            if (!session.delete()) {
356                LOGGER.log(Level.WARNING, "Deleting raw OMEMO session " + session.getAbsolutePath() + "failed.");
357            }
358        }
359    }
360
361    @Override
362    public boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
363        File session = hierarchy.getContactsSessionPath(userDevice, contactsDevice);
364        return session.exists();
365    }
366
367    @Override
368    public void storeOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice, int counter) throws IOException {
369        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
370        writeIntegers(messageCounterFile, Collections.singleton(counter));
371    }
372
373    @Override
374    public int loadOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
375        File messageCounterFile = hierarchy.getDevicesMessageCounterPath(userDevice, contactsDevice);
376        Set<Integer> integers = readIntegers(messageCounterFile);
377
378        if (integers == null || integers.isEmpty()) {
379            return 0;
380        }
381
382        return integers.iterator().next();
383    }
384
385    @Override
386    public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) throws IOException {
387        OmemoCachedDeviceList cachedDeviceList = new OmemoCachedDeviceList();
388
389        if (contact == null) {
390            throw new IllegalArgumentException("Contact can not be null.");
391        }
392
393        // active
394        File activeDevicesPath = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
395        Set<Integer> active = readIntegers(activeDevicesPath);
396        if (active != null) {
397            cachedDeviceList.getActiveDevices().addAll(active);
398        }
399
400        // inactive
401        File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
402        Set<Integer> inactive = readIntegers(inactiveDevicesPath);
403        if (inactive != null) {
404            cachedDeviceList.getInactiveDevices().addAll(inactive);
405        }
406
407        return cachedDeviceList;
408    }
409
410    @Override
411    public void storeCachedDeviceList(OmemoDevice userDevice,
412                                      BareJid contact,
413                                      OmemoCachedDeviceList contactsDeviceList) throws IOException {
414        if (contact == null) {
415            return;
416        }
417
418        File activeDevices = hierarchy.getContactsActiveDevicesPath(userDevice, contact);
419        writeIntegers(activeDevices, contactsDeviceList.getActiveDevices());
420
421        File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(userDevice, contact);
422        writeIntegers(inactiveDevices, contactsDeviceList.getInactiveDevices());
423    }
424
425    @Override
426    public void purgeOwnDeviceKeys(OmemoDevice userDevice) {
427        File deviceDirectory = hierarchy.getUserDeviceDirectory(userDevice);
428        deleteDirectory(deviceDirectory);
429    }
430
431    private static void writeLong(File target, long i) throws IOException {
432        if (target == null) {
433            throw new IOException("Could not write long to null-path.");
434        }
435
436        FileHierarchy.createFile(target);
437
438        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
439            out.writeLong(i);
440        }
441    }
442
443    private static Long readLong(File target) throws IOException {
444        if (target == null) {
445            throw new IOException("Could not read long from null-path.");
446        }
447
448        if (!target.exists() || !target.isFile()) {
449            return null;
450        }
451
452        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
453            return in.readLong();
454        }
455    }
456
457    private static void writeBytes(File target, byte[] bytes) throws IOException {
458        if (target == null) {
459            throw new IOException("Could not write bytes to null-path.");
460        }
461
462        // Create file
463        FileHierarchy.createFile(target);
464
465        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
466            out.write(bytes);
467        }
468    }
469
470    private static byte[] readBytes(File target) throws IOException {
471        if (target == null) {
472            throw new IOException("Could not read bytes from null-path.");
473        }
474
475        if (!target.exists() || !target.isFile()) {
476            return null;
477        }
478
479        byte[] b = new byte[(int) target.length()];
480        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
481            in.read(b);
482        }
483
484        return b;
485    }
486
487    private static void writeIntegers(File target, Set<Integer> integers) throws IOException {
488        if (target == null) {
489            throw new IOException("Could not write integers to null-path.");
490        }
491
492        FileHierarchy.createFile(target);
493
494        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) {
495            for (int i : integers) {
496                out.writeInt(i);
497            }
498        }
499    }
500
501    private static Set<Integer> readIntegers(File target) throws IOException {
502        if (target == null) {
503            throw new IOException("Could not write integers to null-path.");
504        }
505
506        if (!target.exists() || !target.isFile()) {
507            return null;
508        }
509
510        HashSet<Integer> integers = new HashSet<>();
511
512        try (DataInputStream in = new DataInputStream(new FileInputStream(target))) {
513            while (true) {
514                try {
515                    integers.add(in.readInt());
516                } catch (EOFException e) {
517                    break;
518                }
519            }
520        }
521
522        return integers;
523    }
524
525    /**
526     * Delete a directory with all subdirectories.
527     * @param root directory to be deleted
528     */
529    @SuppressWarnings("JdkObsolete")
530    public static void deleteDirectory(File root) {
531        File[] currList;
532        Stack<File> stack = new Stack<>();
533        stack.push(root);
534        while (!stack.isEmpty()) {
535            if (stack.lastElement().isDirectory()) {
536                currList = stack.lastElement().listFiles();
537                if (currList != null && currList.length > 0) {
538                    for (File curr : currList) {
539                        stack.push(curr);
540                    }
541                } else {
542                    stack.pop().delete();
543                }
544            } else {
545                stack.pop().delete();
546            }
547        }
548    }
549
550    /**
551     * This class represents the directory structure of the FileBasedOmemoStore.
552     * The directory looks as follows:
553     *
554     *  OMEMO_Store/
555     *      'romeo@montague.lit'/                           //Our bareJid
556     *          ...
557     *      'juliet@capulet.lit'/                           //Our other bareJid
558     *          '13371234'/                                 //deviceId
559     *              identityKeyPair                         //Our identityKeyPair
560     *              lastSignedPreKeyRenewal                 //Date of when the signedPreKey was last renewed.
561     *              preKeys/                                //Our preKeys
562     *                  '1'
563     *                  '2'
564     *                  ...
565     *              signedPreKeys/                          //Our signedPreKeys
566     *                  '1'
567     *                  '2'
568     *                  ...
569     *              contacts/
570     *                  'romeo@capulet.lit'/                //Juliets contact Romeo
571     *                      activeDevice                    //List of Romeos active devices
572     *                      inactiveDevices                 //List of his inactive devices
573     *                      'deviceId'/                     //Romeos deviceId
574     *                          identityKey                 //Romeos identityKey
575     *                          session                     //Our session with romeo
576     *                          trust                       //Records about the trust in romeos device
577     *                          (lastReceivedMessageDate)   //Only, for our own other devices:
578     *                                                          //date of the last received message
579     *
580     */
581    public static class FileHierarchy {
582
583        static final String STORE = "OMEMO_Store";
584        static final String CONTACTS = "contacts";
585        static final String IDENTITY_KEY = "identityKey";
586        static final String IDENTITY_KEY_PAIR = "identityKeyPair";
587        static final String PRE_KEYS = "preKeys";
588        static final String LAST_MESSAGE_RECEVIED_DATE = "lastMessageReceivedDate";
589        static final String LAST_DEVICEID_PUBLICATION_DATE = "lastDeviceIdPublicationDate";
590        static final String SIGNED_PRE_KEYS = "signedPreKeys";
591        static final String LAST_SIGNED_PRE_KEY_RENEWAL = "lastSignedPreKeyRenewal";
592        static final String SESSION = "session";
593        static final String DEVICE_LIST_ACTIVE = "activeDevices";
594        static final String DEVICE_LIST_INAVTIVE = "inactiveDevices";
595        static final String MESSAGE_COUNTER = "messageCounter";
596
597        File basePath;
598
599        FileHierarchy(File basePath) {
600            this.basePath = basePath;
601            basePath.mkdirs();
602        }
603
604        File getStoreDirectory() {
605            return createDirectory(basePath, STORE);
606        }
607
608        File getUserDirectory(OmemoDevice userDevice) {
609            return getUserDirectory(userDevice.getJid());
610        }
611
612        File getUserDirectory(BareJid bareJid) {
613            return createDirectory(getStoreDirectory(), bareJidEncoder.encode(bareJid));
614        }
615
616        File getUserDeviceDirectory(OmemoDevice userDevice) {
617            return createDirectory(getUserDirectory(userDevice.getJid()),
618                    Integer.toString(userDevice.getDeviceId()));
619        }
620
621        File getContactsDir(OmemoDevice userDevice) {
622            return createDirectory(getUserDeviceDirectory(userDevice), CONTACTS);
623        }
624
625        File getContactsDir(OmemoDevice userDevice, BareJid contact) {
626            return createDirectory(getContactsDir(userDevice), bareJidEncoder.encode(contact));
627        }
628
629        File getContactsDir(OmemoDevice userDevice, OmemoDevice contactsDevice) {
630            return createDirectory(getContactsDir(userDevice, contactsDevice.getJid()),
631                    Integer.toString(contactsDevice.getDeviceId()));
632        }
633
634        File getIdentityKeyPairPath(OmemoDevice userDevice) {
635            return new File(getUserDeviceDirectory(userDevice), IDENTITY_KEY_PAIR);
636        }
637
638        File getPreKeysDirectory(OmemoDevice userDevice) {
639            return createDirectory(getUserDeviceDirectory(userDevice), PRE_KEYS);
640        }
641
642        File getPreKeyPath(OmemoDevice userDevice, int preKeyId) {
643            return new File(getPreKeysDirectory(userDevice), Integer.toString(preKeyId));
644        }
645
646        File getLastMessageReceivedDatePath(OmemoDevice userDevice, OmemoDevice device) {
647            return new File(getContactsDir(userDevice, device), LAST_MESSAGE_RECEVIED_DATE);
648        }
649
650        File getLastDeviceIdPublicationDatePath(OmemoDevice userDevice, OmemoDevice device) {
651            return new File(getContactsDir(userDevice, device), LAST_DEVICEID_PUBLICATION_DATE);
652        }
653
654        File getSignedPreKeysDirectory(OmemoDevice userDevice) {
655            return createDirectory(getUserDeviceDirectory(userDevice), SIGNED_PRE_KEYS);
656        }
657
658        File getLastSignedPreKeyRenewal(OmemoDevice userDevice) {
659            return new File(getUserDeviceDirectory(userDevice), LAST_SIGNED_PRE_KEY_RENEWAL);
660        }
661
662        File getContactsIdentityKeyPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
663            return new File(getContactsDir(userDevice, contactsDevice), IDENTITY_KEY);
664
665        }
666
667        File getContactsSessionPath(OmemoDevice userDevice, OmemoDevice contactsDevice) {
668            return new File(getContactsDir(userDevice, contactsDevice), SESSION);
669        }
670
671        File getContactsActiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
672            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_ACTIVE);
673        }
674
675        File getContactsInactiveDevicesPath(OmemoDevice userDevice, BareJid contact) {
676            return new File(getContactsDir(userDevice, contact), DEVICE_LIST_INAVTIVE);
677        }
678
679        File getDevicesMessageCounterPath(OmemoDevice userDevice, OmemoDevice otherDevice) {
680            return new File(getContactsDir(userDevice, otherDevice), MESSAGE_COUNTER);
681        }
682
683        private static File createFile(File f) throws IOException {
684            File p = f.getParentFile();
685            createDirectory(p);
686            f.createNewFile();
687            return f;
688
689        }
690
691        private static File createDirectory(File dir, String subdir) {
692            File f = new File(dir, subdir);
693            return createDirectory(f);
694        }
695
696        private static File createDirectory(File f) {
697            if (f.exists() && f.isDirectory()) {
698                return f;
699            }
700
701            f.mkdirs();
702            return f;
703        }
704    }
705
706    /**
707     * Convert {@link BareJid BareJids} to Strings using the legacy {@link BareJid#toString()} method instead of the
708     * proper, url safe {@link BareJid#asUrlEncodedString()} method.
709     * While it is highly advised to use the new format, you can use this method to stay backwards compatible to data
710     * sets created by the old implementation.
711     */
712    @SuppressWarnings("deprecation")
713    public static void useLegacyBareJidEncoding() {
714        bareJidEncoder = new BareJidEncoder.LegacyEncoder();
715    }
716}