Reorganize session store load/store operations.
parent
d902c12941
commit
14b8f97de2
@ -0,0 +1,54 @@
|
||||
package org.whispersystems.test;
|
||||
|
||||
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||
import org.whispersystems.libaxolotl.state.SessionState;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class InMemorySessionRecord implements SessionRecord {
|
||||
|
||||
private SessionState currentSessionState;
|
||||
private List<SessionState> previousSessionStates;
|
||||
|
||||
public InMemorySessionRecord() {
|
||||
currentSessionState = new InMemorySessionState();
|
||||
previousSessionStates = new LinkedList<>();
|
||||
}
|
||||
|
||||
public InMemorySessionRecord(SessionRecord copy) {
|
||||
currentSessionState = new InMemorySessionState(copy.getSessionState());
|
||||
previousSessionStates = new LinkedList<>();
|
||||
|
||||
for (SessionState previousState : copy.getPreviousSessionStates()) {
|
||||
previousSessionStates.add(new InMemorySessionState(previousState));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionState getSessionState() {
|
||||
return currentSessionState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionState> getPreviousSessionStates() {
|
||||
return previousSessionStates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.currentSessionState = new InMemorySessionState();
|
||||
this.previousSessionStates = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void archiveCurrentState() {
|
||||
this.previousSessionStates.add(currentSessionState);
|
||||
this.currentSessionState = new InMemorySessionState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
@ -1,44 +1,63 @@
|
||||
package org.whispersystems.test;
|
||||
|
||||
import org.whispersystems.libaxolotl.SessionState;
|
||||
import org.whispersystems.libaxolotl.SessionStore;
|
||||
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||
import org.whispersystems.libaxolotl.util.Pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class InMemorySessionStore implements SessionStore {
|
||||
|
||||
private SessionState currentSessionState;
|
||||
private List<SessionState> previousSessionStates;
|
||||
private Map<Pair<Long, Integer>, SessionRecord> sessions = new HashMap<>();
|
||||
|
||||
private SessionState checkedOutSessionState;
|
||||
private List<SessionState> checkedOutPreviousSessionStates;
|
||||
public InMemorySessionStore() {}
|
||||
|
||||
public InMemorySessionStore(SessionState sessionState) {
|
||||
this.currentSessionState = sessionState;
|
||||
this.previousSessionStates = new LinkedList<>();
|
||||
this.checkedOutPreviousSessionStates = new LinkedList<>();
|
||||
@Override
|
||||
public SessionRecord get(long recipientId, int deviceId) {
|
||||
if (contains(recipientId, deviceId)) {
|
||||
return new InMemorySessionRecord(sessions.get(new Pair<>(recipientId, deviceId)));
|
||||
} else {
|
||||
return new InMemorySessionRecord();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionState getSessionState() {
|
||||
checkedOutSessionState = new InMemorySessionState(currentSessionState);
|
||||
return checkedOutSessionState;
|
||||
public List<Integer> getSubDeviceSessions(long recipientId) {
|
||||
List<Integer> deviceIds = new LinkedList<>();
|
||||
|
||||
for (Pair<Long, Integer> key : sessions.keySet()) {
|
||||
if (key.first() == recipientId) {
|
||||
deviceIds.add(key.second());
|
||||
}
|
||||
}
|
||||
|
||||
return deviceIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionState> getPreviousSessionStates() {
|
||||
checkedOutPreviousSessionStates = new LinkedList<>();
|
||||
for (SessionState state : previousSessionStates) {
|
||||
checkedOutPreviousSessionStates.add(new InMemorySessionState(state));
|
||||
}
|
||||
public void put(long recipientId, int deviceId, SessionRecord record) {
|
||||
sessions.put(new Pair<>(recipientId, deviceId), record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(long recipientId, int deviceId) {
|
||||
return sessions.containsKey(new Pair<>(recipientId, deviceId));
|
||||
}
|
||||
|
||||
return checkedOutPreviousSessionStates;
|
||||
@Override
|
||||
public void delete(long recipientId, int deviceId) {
|
||||
sessions.remove(new Pair<>(recipientId, deviceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
this.currentSessionState = this.checkedOutSessionState;
|
||||
this.previousSessionStates = this.checkedOutPreviousSessionStates;
|
||||
public void deleteAll(long recipientId) {
|
||||
for (Pair<Long, Integer> key : sessions.keySet()) {
|
||||
if (key.first() == recipientId) {
|
||||
sessions.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
package org.whispersystems.libaxolotl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SessionStore {
|
||||
|
||||
public SessionState getSessionState();
|
||||
public List<SessionState> getPreviousSessionStates();
|
||||
public void save();
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.whispersystems.libaxolotl.state;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SessionRecord {
|
||||
|
||||
public SessionState getSessionState();
|
||||
public List<SessionState> getPreviousSessionStates();
|
||||
public void reset();
|
||||
public void archiveCurrentState();
|
||||
public byte[] serialize();
|
||||
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package org.whispersystems.libaxolotl;
|
||||
package org.whispersystems.libaxolotl.state;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
@ -0,0 +1,14 @@
|
||||
package org.whispersystems.libaxolotl.state;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SessionStore {
|
||||
|
||||
public SessionRecord get(long recipientId, int deviceId);
|
||||
public List<Integer> getSubDeviceSessions(long recipientId);
|
||||
public void put(long recipientId, int deviceId, SessionRecord record);
|
||||
public boolean contains(long recipientId, int deviceId);
|
||||
public void delete(long recipientId, int deviceId);
|
||||
public void deleteAll(long recipientId);
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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() + "");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue