187 lines
6.0 KiB
Java
187 lines
6.0 KiB
Java
package org.thoughtcrime.securesms.crypto.storage;
|
|
|
|
import android.content.Context;
|
|
import android.util.Log;
|
|
|
|
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
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.PushAddress;
|
|
import org.thoughtcrime.securesms.util.Conversions;
|
|
|
|
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(long recipientId, int deviceId) {
|
|
synchronized (FILE_LOCK) {
|
|
try {
|
|
MasterCipher cipher = new MasterCipher(masterSecret);
|
|
FileInputStream in = new FileInputStream(getSessionFile(recipientId, deviceId));
|
|
|
|
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(long recipientId, int deviceId, SessionRecord record) {
|
|
synchronized (FILE_LOCK) {
|
|
try {
|
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
|
RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(recipientId, deviceId), "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(long recipientId, int deviceId) {
|
|
return getSessionFile(recipientId, deviceId).exists() &&
|
|
loadSession(recipientId, deviceId).getSessionState().hasSenderChain();
|
|
}
|
|
|
|
@Override
|
|
public void deleteSession(long recipientId, int deviceId) {
|
|
getSessionFile(recipientId, deviceId).delete();
|
|
}
|
|
|
|
@Override
|
|
public void deleteAllSessions(long recipientId) {
|
|
List<Integer> devices = getSubDeviceSessions(recipientId);
|
|
|
|
deleteSession(recipientId, PushAddress.DEFAULT_DEVICE_ID);
|
|
|
|
for (int device : devices) {
|
|
deleteSession(recipientId, device);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public List<Integer> getSubDeviceSessions(long recipientId) {
|
|
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("SessionRecordV2", e);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private File getSessionFile(long recipientId, int deviceId) {
|
|
return new File(getSessionDirectory(), getSessionName(recipientId, deviceId));
|
|
}
|
|
|
|
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(long recipientId, int deviceId) {
|
|
return recipientId + (deviceId == PushAddress.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));
|
|
}
|
|
|
|
}
|