Reorganize session store load/store operations.

This commit is contained in:
Moxie Marlinspike
2014-04-22 14:33:29 -07:00
parent d902c12941
commit 14b8f97de2
37 changed files with 666 additions and 635 deletions

View File

@@ -20,8 +20,9 @@ package org.whispersystems.textsecure.crypto;
import android.content.Context;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
public class SessionCipherFactory {
@@ -29,9 +30,10 @@ public class SessionCipherFactory {
MasterSecret masterSecret,
RecipientDevice recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
SessionRecordV2 record = new SessionRecordV2(context, masterSecret, recipient);
return new SessionCipher(record);
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
if (sessionStore.contains(recipient.getRecipientId(), recipient.getDeviceId())) {
return new SessionCipher(sessionStore, recipient.getRecipientId(), recipient.getDeviceId());
} else {
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
}

View File

@@ -1,80 +0,0 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.legacy.LocalKeyRecord;
import org.whispersystems.textsecure.storage.legacy.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.legacy.SessionRecordV1;
/**
* Helper class for generating key pairs and calculating ECDH agreements.
*
* @author Moxie Marlinspike
*/
public class Session {
public static void clearV1SessionFor(Context context, CanonicalRecipient recipient) {
//XXX Obviously we should probably do something more thorough here eventually.
LocalKeyRecord.delete(context, recipient);
RemoteKeyRecord.delete(context, recipient);
SessionRecordV1.delete(context, recipient);
}
public static void abortSessionFor(Context context, CanonicalRecipient recipient) {
Log.w("Session", "Aborting session, deleting keys...");
clearV1SessionFor(context, recipient);
SessionRecordV2.deleteAll(context, recipient);
}
public static boolean hasSession(Context context, MasterSecret masterSecret,
CanonicalRecipient recipient)
{
Log.w("Session", "Checking session...");
return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID);
}
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient)
{
RecipientDevice device = new RecipientDevice(recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID);
return hasEncryptCapableSession(context, masterSecret, recipient, device);
}
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient,
RecipientDevice device)
{
return hasSession(context, masterSecret, recipient) &&
!SessionRecordV2.needsRefresh(context, masterSecret, device);
}
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
CanonicalRecipient recipient)
{
return getRemoteIdentityKey(context, masterSecret, recipient.getRecipientId());
}
public static IdentityKey getRemoteIdentityKey(Context context,
MasterSecret masterSecret,
long recipientId)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipientId,
RecipientDevice.DEFAULT_DEVICE_ID))
{
return new SessionRecordV2(context, masterSecret, recipientId,
RecipientDevice.DEFAULT_DEVICE_ID).getSessionState()
.getRemoteIdentityKey();
} else {
return null;
}
}
}

View File

@@ -1,229 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.SessionState;
import org.whispersystems.libaxolotl.SessionStore;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.storage.StorageProtos.RecordStructure;
import static org.whispersystems.textsecure.storage.StorageProtos.SessionStructure;
/**
* A disk record representing a current session.
*
* @author Moxie Marlinspike
*/
public class SessionRecordV2 extends Record implements SessionStore {
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 MasterSecret masterSecret;
private TextSecureSessionState sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
private List<SessionState> previousStates = new LinkedList<SessionState>();
public SessionRecordV2(Context context, MasterSecret masterSecret, RecipientDevice peer) {
this(context, masterSecret, peer.getRecipientId(), peer.getDeviceId());
}
public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId, int deviceId) {
super(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId));
this.masterSecret = masterSecret;
loadData();
}
private static String getRecordName(long recipientId, int deviceId) {
return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
}
public TextSecureSessionState getSessionState() {
return sessionState;
}
public List<SessionState> getPreviousSessionStates() {
return previousStates;
}
public static List<Integer> getSessionSubDevices(Context context, CanonicalRecipient recipient) {
List<Integer> results = new LinkedList<Integer>();
File parent = getParentDirectory(context, SESSIONS_DIRECTORY_V2);
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 == recipient.getRecipientId() && parts.length > 1) {
results.add(Integer.parseInt(parts[1]));
}
} catch (NumberFormatException e) {
Log.w("SessionRecordV2", e);
}
}
return results;
}
public static void deleteAll(Context context, CanonicalRecipient recipient) {
List<Integer> devices = getSessionSubDevices(context, recipient);
delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID));
for (int device : devices) {
delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(), device));
}
}
public static void delete(Context context, RecipientDevice recipientDevice) {
delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientDevice.getRecipientId(),
recipientDevice.getDeviceId()));
}
public static boolean hasSession(Context context, MasterSecret masterSecret,
RecipientDevice recipient)
{
return hasSession(context, masterSecret, recipient.getRecipientId(), recipient.getDeviceId());
}
public static boolean hasSession(Context context, MasterSecret masterSecret,
long recipientId, int deviceId)
{
return hasRecord(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)) &&
new SessionRecordV2(context, masterSecret, recipientId, deviceId).sessionState.hasSenderChain();
}
public static boolean needsRefresh(Context context, MasterSecret masterSecret,
RecipientDevice recipient)
{
return new SessionRecordV2(context, masterSecret,
recipient.getRecipientId(),
recipient.getDeviceId()).getSessionState()
.getNeedsRefresh();
}
public void clear() {
this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
this.previousStates = new LinkedList<SessionState>();
}
public void archiveCurrentState() {
this.previousStates.add(sessionState);
this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
}
public void save() {
synchronized (FILE_LOCK) {
try {
List<SessionStructure> previousStructures = new LinkedList<SessionStructure>();
for (SessionState previousState : previousStates) {
previousStructures.add(((TextSecureSessionState)previousState).getStructure());
}
RecordStructure record = RecordStructure.newBuilder()
.setCurrentSession(sessionState.getStructure())
.addAllPreviousSessions(previousStructures)
.build();
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
MasterCipher cipher = new MasterCipher(masterSecret);
writeInteger(CURRENT_VERSION, out);
writeBlob(cipher.encryptBytes(record.toByteArray()), out);
out.truncate(out.position());
file.close();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
private void loadData() {
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
int versionMarker = readInteger(in);
if (versionMarker > CURRENT_VERSION) {
throw new AssertionError("Unknown version: " + versionMarker);
}
MasterCipher cipher = new MasterCipher(masterSecret);
byte[] encryptedBlob = readBlob(in);
if (versionMarker == SINGLE_STATE_VERSION) {
byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob);
SessionStructure sessionStructure = SessionStructure.parseFrom(plaintextBytes);
this.sessionState = new TextSecureSessionState(sessionStructure);
} else if (versionMarker == ARCHIVE_STATES_VERSION) {
byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob);
RecordStructure recordStructure = RecordStructure.parseFrom(plaintextBytes);
this.sessionState = new TextSecureSessionState(recordStructure.getCurrentSession());
this.previousStates = new LinkedList<SessionState>();
for (SessionStructure sessionStructure : recordStructure.getPreviousSessionsList()) {
this.previousStates.add(new TextSecureSessionState(sessionStructure));
}
} else {
throw new AssertionError("Unknown version: " + versionMarker);
}
in.close();
} catch (FileNotFoundException e) {
Log.w("SessionRecordV2", "No session information found.");
// XXX
} catch (IOException ioe) {
Log.w("SessionRecordV2", ioe);
// XXX
} catch (InvalidMessageException ime) {
Log.w("SessionRecordV2", ime);
// XXX
}
}
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
public class SessionUtil {
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient)
{
return hasEncryptCapableSession(context, masterSecret,
new RecipientDevice(recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID));
}
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
RecipientDevice recipientDevice)
{
long recipientId = recipientDevice.getRecipientId();
int deviceId = recipientDevice.getDeviceId();
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
return
sessionStore.contains(recipientId, deviceId) &&
!sessionStore.get(recipientId, deviceId).getSessionState().getNeedsRefresh();
}
}

View File

@@ -0,0 +1,142 @@
package org.whispersystems.textsecure.storage;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionState;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Conversions;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.storage.StorageProtos.RecordStructure;
import static org.whispersystems.textsecure.storage.StorageProtos.SessionStructure;
public class TextSecureSessionRecord implements SessionRecord {
private static final int SINGLE_STATE_VERSION = 1;
private static final int ARCHIVE_STATES_VERSION = 2;
private static final int CURRENT_VERSION = 2;
private TextSecureSessionState sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
private List<SessionState> previousStates = new LinkedList<>();
private final MasterSecret masterSecret;
public TextSecureSessionRecord(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
}
public TextSecureSessionRecord(MasterSecret masterSecret, FileInputStream in)
throws IOException, InvalidMessageException
{
this.masterSecret = masterSecret;
int versionMarker = readInteger(in);
if (versionMarker > CURRENT_VERSION) {
throw new AssertionError("Unknown version: " + versionMarker);
}
MasterCipher cipher = new MasterCipher(masterSecret);
byte[] encryptedBlob = readBlob(in);
if (versionMarker == SINGLE_STATE_VERSION) {
byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob);
SessionStructure sessionStructure = SessionStructure.parseFrom(plaintextBytes);
this.sessionState = new TextSecureSessionState(sessionStructure);
} else if (versionMarker == ARCHIVE_STATES_VERSION) {
byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob);
RecordStructure recordStructure = RecordStructure.parseFrom(plaintextBytes);
this.sessionState = new TextSecureSessionState(recordStructure.getCurrentSession());
this.previousStates = new LinkedList<>();
for (SessionStructure sessionStructure : recordStructure.getPreviousSessionsList()) {
this.previousStates.add(new TextSecureSessionState(sessionStructure));
}
} else {
throw new AssertionError("Unknown version: " + versionMarker);
}
in.close();
}
@Override
public SessionState getSessionState() {
return sessionState;
}
@Override
public List<SessionState> getPreviousSessionStates() {
return previousStates;
}
@Override
public void reset() {
this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
this.previousStates = new LinkedList<>();
}
@Override
public void archiveCurrentState() {
this.previousStates.add(sessionState);
this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
}
@Override
public byte[] serialize() {
try {
List<SessionStructure> previousStructures = new LinkedList<>();
for (SessionState previousState : previousStates) {
previousStructures.add(((TextSecureSessionState)previousState).getStructure());
}
RecordStructure record = RecordStructure.newBuilder()
.setCurrentSession(sessionState.getStructure())
.addAllPreviousSessions(previousStructures)
.build();
ByteArrayOutputStream serialized = new ByteArrayOutputStream();
MasterCipher cipher = new MasterCipher(masterSecret);
writeInteger(CURRENT_VERSION, serialized);
writeBlob(cipher.encryptBytes(record.toByteArray()), serialized);
return serialized.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
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, OutputStream out) throws IOException {
writeInteger(blobBytes.length, out);
out.write(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, OutputStream out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
out.write(valueBytes);
}
}

View File

@@ -23,7 +23,7 @@ import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.SessionState;
import org.whispersystems.libaxolotl.state.SessionState;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;

View File

@@ -0,0 +1,130 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
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;
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 final Context context;
private final MasterSecret masterSecret;
public TextSecureSessionStore(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
@Override
public SessionRecord get(long recipientId, int deviceId) {
synchronized (FILE_LOCK) {
try {
FileInputStream input = new FileInputStream(getSessionFile(recipientId, deviceId));
return new TextSecureSessionRecord(masterSecret, input);
} catch (InvalidMessageException | IOException e) {
Log.w(TAG, "No existing session information found.");
return new TextSecureSessionRecord(masterSecret);
}
}
}
@Override
public void put(long recipientId, int deviceId, SessionRecord record) {
try {
RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(recipientId, deviceId), "rw");
FileChannel out = sessionFile.getChannel();
out.position(0);
out.write(ByteBuffer.wrap(record.serialize()));
out.truncate(out.position());
sessionFile.close();
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public boolean contains(long recipientId, int deviceId) {
return getSessionFile(recipientId, deviceId).exists() &&
get(recipientId, deviceId).getSessionState().hasSenderChain();
}
@Override
public void delete(long recipientId, int deviceId) {
getSessionFile(recipientId, deviceId).delete();
}
@Override
public void deleteAll(long recipientId) {
List<Integer> devices = getSubDeviceSessions(recipientId);
delete(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
for (int device : devices) {
delete(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 == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
}
}

View File

@@ -1,34 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.storage.legacy;
import android.content.Context;
import org.whispersystems.textsecure.storage.CanonicalRecipient;
import org.whispersystems.textsecure.storage.Record;
public class LocalKeyRecord {
public static void delete(Context context, CanonicalRecipient recipient) {
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getRecipientId() + "-local";
}
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.storage.legacy;
import android.content.Context;
import org.whispersystems.textsecure.storage.CanonicalRecipient;
import org.whispersystems.textsecure.storage.Record;
/**
* Represents the current and last public key belonging to the "remote"
* endpoint in an encrypted session. These are stored on disk.
*
* @author Moxie Marlinspike
*/
public class RemoteKeyRecord {
public static void delete(Context context, CanonicalRecipient recipient) {
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getRecipientId() + "-remote";
}
}

View File

@@ -1,18 +0,0 @@
package org.whispersystems.textsecure.storage.legacy;
import android.content.Context;
import org.whispersystems.textsecure.storage.CanonicalRecipient;
import org.whispersystems.textsecure.storage.Record;
/**
* A disk record representing a current session.
*
* @author Moxie Marlinspike
*/
public class SessionRecordV1 {
public static void delete(Context context, CanonicalRecipient recipient) {
Record.delete(context, Record.SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
}
}