package org.thoughtcrime.SMP.crypto.storage; import android.content.Context; import android.util.Log; import org.thoughtcrime.SMP.crypto.MasterCipher; import org.thoughtcrime.SMP.crypto.MasterSecret; import org.thoughtcrime.SMP.recipients.Recipient; import org.thoughtcrime.SMP.recipients.RecipientFactory; import org.thoughtcrime.SMP.util.Conversions; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.api.push.TextSecureAddress; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.LinkedList; import java.util.List; import static org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure; public class TextSecureSessionStore implements SessionStore { private static final String TAG = TextSecureSessionStore.class.getSimpleName(); private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2"; private static final Object FILE_LOCK = new Object(); private static final int SINGLE_STATE_VERSION = 1; private static final int ARCHIVE_STATES_VERSION = 2; private static final int CURRENT_VERSION = 2; private final Context context; private final MasterSecret masterSecret; public TextSecureSessionStore(Context context, MasterSecret masterSecret) { this.context = context.getApplicationContext(); this.masterSecret = masterSecret; } @Override public SessionRecord loadSession(AxolotlAddress address) { synchronized (FILE_LOCK) { try { MasterCipher cipher = new MasterCipher(masterSecret); FileInputStream in = new FileInputStream(getSessionFile(address)); int versionMarker = readInteger(in); if (versionMarker > CURRENT_VERSION) { throw new AssertionError("Unknown version: " + versionMarker); } byte[] serialized = cipher.decryptBytes(readBlob(in)); in.close(); if (versionMarker == SINGLE_STATE_VERSION) { SessionStructure sessionStructure = SessionStructure.parseFrom(serialized); SessionState sessionState = new SessionState(sessionStructure); return new SessionRecord(sessionState); } else if (versionMarker == ARCHIVE_STATES_VERSION) { return new SessionRecord(serialized); } else { throw new AssertionError("Unknown version: " + versionMarker); } } catch (InvalidMessageException | IOException e) { Log.w(TAG, "No existing session information found."); return new SessionRecord(); } } } @Override public void storeSession(AxolotlAddress address, SessionRecord record) { synchronized (FILE_LOCK) { try { MasterCipher masterCipher = new MasterCipher(masterSecret); RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(address), "rw"); FileChannel out = sessionFile.getChannel(); out.position(0); writeInteger(CURRENT_VERSION, out); writeBlob(masterCipher.encryptBytes(record.serialize()), out); out.truncate(out.position()); sessionFile.close(); } catch (IOException e) { throw new AssertionError(e); } } } @Override public boolean containsSession(AxolotlAddress address) { return getSessionFile(address).exists() && loadSession(address).getSessionState().hasSenderChain(); } @Override public void deleteSession(AxolotlAddress address) { getSessionFile(address).delete(); } @Override public void deleteAllSessions(String name) { List<Integer> devices = getSubDeviceSessions(name); deleteSession(new AxolotlAddress(name, TextSecureAddress.DEFAULT_DEVICE_ID)); for (int device : devices) { deleteSession(new AxolotlAddress(name, device)); } } @Override public List<Integer> getSubDeviceSessions(String name) { long recipientId = RecipientFactory.getRecipientsFromString(context, name, true).getPrimaryRecipient().getRecipientId(); List<Integer> results = new LinkedList<>(); File parent = getSessionDirectory(); String[] children = parent.list(); if (children == null) return results; for (String child : children) { try { String[] parts = child.split("[.]", 2); long sessionRecipientId = Long.parseLong(parts[0]); if (sessionRecipientId == recipientId && parts.length > 1) { results.add(Integer.parseInt(parts[1])); } } catch (NumberFormatException e) { Log.w(TAG, e); } } return results; } private File getSessionFile(AxolotlAddress address) { return new File(getSessionDirectory(), getSessionName(address)); } private File getSessionDirectory() { File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2); if (!directory.exists()) { if (!directory.mkdirs()) { Log.w(TAG, "Session directory creation failed!"); } } return directory; } private String getSessionName(AxolotlAddress axolotlAddress) { Recipient recipient = RecipientFactory.getRecipientsFromString(context, axolotlAddress.getName(), true) .getPrimaryRecipient(); long recipientId = recipient.getRecipientId(); int deviceId = axolotlAddress.getDeviceId(); return recipientId + (deviceId == TextSecureAddress.DEFAULT_DEVICE_ID ? "" : "." + deviceId); } private byte[] readBlob(FileInputStream in) throws IOException { int length = readInteger(in); byte[] blobBytes = new byte[length]; in.read(blobBytes, 0, blobBytes.length); return blobBytes; } private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException { writeInteger(blobBytes.length, out); out.write(ByteBuffer.wrap(blobBytes)); } private int readInteger(FileInputStream in) throws IOException { byte[] integer = new byte[4]; in.read(integer, 0, integer.length); return Conversions.byteArrayToInt(integer); } private void writeInteger(int value, FileChannel out) throws IOException { byte[] valueBytes = Conversions.intToByteArray(value); out.write(ByteBuffer.wrap(valueBytes)); } }