Only use MasterSecret for local message encryption.

Not for the axolotl store.

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2015-07-06 17:36:49 -07:00
parent b1810e2c44
commit 8d9ae731ef
46 changed files with 847 additions and 616 deletions

View File

@@ -61,39 +61,45 @@ public class AsymmetricMasterCipher {
this.asymmetricMasterSecret = asymmetricMasterSecret;
}
public String decryptBody(String body) throws IOException, InvalidMessageException {
public byte[] encryptBytes(byte[] body) {
try {
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
ECKeyPair ourKeyPair = Curve.generateKeyPair();
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
MasterCipher masterCipher = getMasterCipherForSecret(secret);
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body);
PublicKey ourPublicKey = new PublicKey(31337, ourKeyPair.getPublicKey());
byte[] publicKeyBytes = ourPublicKey.serialize();
return Util.combine(publicKeyBytes, encryptedBodyBytes);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decryptBytes(byte[] combined) throws IOException, InvalidMessageException {
try {
byte[] combined = Base64.decode(body);
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey();
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
MasterCipher masterCipher = getMasterCipherForSecret(secret);
byte[] decryptedBody = masterCipher.decryptBytes(parts[1]);
return new String(decryptedBody);
} catch (InvalidKeyException | InvalidMessageException ike) {
throw new InvalidMessageException(ike);
return masterCipher.decryptBytes(parts[1]);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public String decryptBody(String body) throws IOException, InvalidMessageException {
byte[] combined = Base64.decode(body);
return new String(decryptBytes(combined));
}
public String encryptBody(String body) {
try {
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
ECKeyPair ourKeyPair = Curve.generateKeyPair();
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
MasterCipher masterCipher = getMasterCipherForSecret(secret);
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body.getBytes());
PublicKey ourPublicKey = new PublicKey(31337, ourKeyPair.getPublicKey());
byte[] publicKeyBytes = ourPublicKey.serialize();
byte[] combined = Util.combine(publicKeyBytes, encryptedBodyBytes);
return Base64.encodeBytes(combined);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
return Base64.encodeBytes(encryptBytes(body.getBytes()));
}
private MasterCipher getMasterCipherForSecret(byte[] secretBytes) {

View File

@@ -20,7 +20,7 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libaxolotl.IdentityKey;
@@ -40,86 +40,109 @@ import java.io.IOException;
public class IdentityKeyUtil {
private static final String IDENTITY_PUBLIC_KEY_DJB_PREF = "pref_identity_public_curve25519";
private static final String IDENTITY_PRIVATE_KEY_DJB_PREF = "pref_identity_private_curve25519";
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
private static final String IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_private_curve25519";
private static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
private static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_DJB_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_DJB_PREF);
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static IdentityKey getIdentityKey(Context context) {
if (!hasIdentityKey(context)) return null;
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_DJB_PREF));
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException ioe) {
Log.w("IdentityKeyUtil", ioe);
return null;
} catch (InvalidKeyException e) {
Log.w("IdentityKeyUtil", e);
return null;
}
}
public static IdentityKeyPair getIdentityKeyPair(Context context,
MasterSecret masterSecret)
{
if (!hasIdentityKey(context))
return null;
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_DJB_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeys(Context context, MasterSecret masterSecret) {
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeys(Context context) {
ECKeyPair djbKeyPair = Curve.generateKeyPair();
MasterCipher masterCipher = new MasterCipher(masterSecret);
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
ECPrivateKey djbPrivateKey = djbKeyPair.getPrivateKey();
save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(djbPrivateKey.serialize()));
}
public static boolean hasCurve25519IdentityKeys(Context context) {
public static void migrateIdentityKeys(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
if (!hasIdentityKey(context)) {
if (hasLegacyIdentityKeys(context)) {
IdentityKeyPair legacyPair = getLegacyIdentityKeyPair(context, masterSecret);
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(legacyPair.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(legacyPair.serialize()));
delete(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF);
delete(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF);
} else {
generateIdentityKeys(context);
}
}
}
private static boolean hasLegacyIdentityKeys(Context context) {
return
retrieve(context, IDENTITY_PUBLIC_KEY_DJB_PREF) != null &&
retrieve(context, IDENTITY_PRIVATE_KEY_DJB_PREF) != null;
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF) != null;
}
public static void generateCurve25519IdentityKeys(Context context, MasterSecret masterSecret) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
ECKeyPair djbKeyPair = Curve.generateKeyPair();
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
private static IdentityKeyPair getLegacyIdentityKeyPair(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF));
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF)));
save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
return new IdentityKeyPair(identityKey, privateKey);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static String retrieve(Context context, String key) {
private static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
public static void save(Context context, String key, String value) {
private static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
private static void delete(Context context, String key) {
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import org.whispersystems.libaxolotl.util.guava.Optional;
public class MasterSecretUnion {
private final Optional<MasterSecret> masterSecret;
private final Optional<AsymmetricMasterSecret> asymmetricMasterSecret;
public MasterSecretUnion(@NonNull MasterSecret masterSecret) {
this.masterSecret = Optional.of(masterSecret);
this.asymmetricMasterSecret = Optional.absent();
}
public MasterSecretUnion(@NonNull AsymmetricMasterSecret asymmetricMasterSecret) {
this.masterSecret = Optional.absent();
this.asymmetricMasterSecret = Optional.of(asymmetricMasterSecret);
}
public Optional<MasterSecret> getMasterSecret() {
return masterSecret;
}
public Optional<AsymmetricMasterSecret> getAsymmetricMasterSecret() {
return asymmetricMasterSecret;
}
}

View File

@@ -19,6 +19,8 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
@@ -121,8 +123,8 @@ public class MasterSecretUtil {
}
}
public static AsymmetricMasterSecret getAsymmetricMasterSecret(Context context,
MasterSecret masterSecret)
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
@Nullable MasterSecret masterSecret)
{
try {
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);

View File

@@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libaxolotl.InvalidMessageException;
import java.io.IOException;
public class MediaKey {
public static String getEncrypted(@NonNull MasterSecretUnion masterSecret, @NonNull byte[] key) {
if (masterSecret.getMasterSecret().isPresent()) {
return Base64.encodeBytes(new MasterCipher(masterSecret.getMasterSecret().get()).encryptBytes(key));
} else {
return "?ASYNC-" + Base64.encodeBytes(new AsymmetricMasterCipher(masterSecret.getAsymmetricMasterSecret().get()).encryptBytes(key));
}
}
public static byte[] getDecrypted(@NonNull MasterSecret masterSecret,
@NonNull AsymmetricMasterSecret asymmetricMasterSecret,
@NonNull String encodedKey)
throws IOException, InvalidMessageException
{
if (encodedKey.startsWith("?ASYNC-")) {
return new AsymmetricMasterCipher(asymmetricMasterSecret).decryptBytes(Base64.decode(encodedKey.substring("?ASYNC-".length())));
} else {
return new MasterCipher(masterSecret).decryptBytes(Base64.decode(encodedKey));
}
}
}

View File

@@ -48,8 +48,8 @@ public class PreKeyUtil {
public static final int BATCH_SIZE = 100;
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
public static List<PreKeyRecord> generatePreKeys(Context context) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context);
List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = getNextPreKeyId(context);
@@ -66,11 +66,10 @@ public class PreKeyUtil {
return records;
}
public static SignedPreKeyRecord generateSignedPreKey(Context context, MasterSecret masterSecret,
IdentityKeyPair identityKeyPair)
public static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair)
{
try {
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context);
int signedPreKeyId = getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
@@ -85,8 +84,8 @@ public class PreKeyUtil {
}
}
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
public static PreKeyRecord generateLastResortKey(Context context) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context);
if (preKeyStore.containsPreKey(Medium.MAX_VALUE)) {
try {

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
@@ -25,11 +24,11 @@ public class TextSecureAxolotlStore implements AxolotlStore {
private final IdentityKeyStore identityKeyStore;
private final SessionStore sessionStore;
public TextSecureAxolotlStore(Context context, MasterSecret masterSecret) {
this.preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
this.signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
this.identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret);
this.sessionStore = new TextSecureSessionStore(context, masterSecret);
public TextSecureAxolotlStore(Context context) {
this.preKeyStore = new TextSecurePreKeyStore(context);
this.signedPreKeyStore = new TextSecurePreKeyStore(context);
this.identityKeyStore = new TextSecureIdentityKeyStore(context);
this.sessionStore = new TextSecureSessionStore(context);
}
@Override

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -13,17 +12,15 @@ import org.whispersystems.libaxolotl.state.IdentityKeyStore;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
private final Context context;
private final MasterSecret masterSecret;
private final Context context;
public TextSecureIdentityKeyStore(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
public TextSecureIdentityKeyStore(Context context) {
this.context = context;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
return IdentityKeyUtil.getIdentityKeyPair(context);
}
@Override
@@ -34,13 +31,13 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
long recipientId = RecipientFactory.getRecipientsFromString(context, name, true).getPrimaryRecipient().getRecipientId();
DatabaseFactory.getIdentityDatabase(context).saveIdentity(masterSecret, recipientId, identityKey);
DatabaseFactory.getIdentityDatabase(context).saveIdentity(recipientId, identityKey);
}
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
long recipientId = RecipientFactory.getRecipientsFromString(context, name, true).getPrimaryRecipient().getRecipientId();
return DatabaseFactory.getIdentityDatabase(context)
.isValidIdentity(masterSecret, recipientId, identityKey);
.isValidIdentity(recipientId, identityKey);
}
}

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
@@ -28,14 +30,19 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
public static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
private static final int CURRENT_VERSION_MARKER = 1;
private static final int PLAINTEXT_VERSION = 2;
private static final int CURRENT_VERSION_MARKER = 2;
private static final Object FILE_LOCK = new Object();
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
@NonNull private final Context context;
@Nullable private final MasterSecret masterSecret;
public TextSecurePreKeyStore(Context context, MasterSecret masterSecret) {
public TextSecurePreKeyStore(@NonNull Context context) {
this(context, null);
}
public TextSecurePreKeyStore(@NonNull Context context, @Nullable MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
}
@@ -129,28 +136,65 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
record.delete();
}
public void migrateRecords() {
synchronized (FILE_LOCK) {
File preKeyRecords = getPreKeyDirectory();
for (File preKeyRecord : preKeyRecords.listFiles()) {
try {
int preKeyId = Integer.parseInt(preKeyRecord.getName());
PreKeyRecord record = loadPreKey(preKeyId);
storePreKey(preKeyId, record);
} catch (InvalidKeyIdException | NumberFormatException e) {
Log.w(TAG, e);
}
}
File signedPreKeyRecords = getSignedPreKeyDirectory();
for (File signedPreKeyRecord : signedPreKeyRecords.listFiles()) {
try {
int signedPreKeyId = Integer.parseInt(signedPreKeyRecord.getName());
SignedPreKeyRecord record = loadSignedPreKey(signedPreKeyId);
storeSignedPreKey(signedPreKeyId, record);
} catch (InvalidKeyIdException | NumberFormatException e) {
Log.w(TAG, e);
}
}
}
}
private byte[] loadSerializedRecord(File recordFile)
throws IOException, InvalidMessageException
{
MasterCipher masterCipher = new MasterCipher(masterSecret);
FileInputStream fin = new FileInputStream(recordFile);
int recordVersion = readInteger(fin);
if (recordVersion != CURRENT_VERSION_MARKER) {
if (recordVersion > CURRENT_VERSION_MARKER) {
throw new AssertionError("Invalid version: " + recordVersion);
}
return masterCipher.decryptBytes(readBlob(fin));
byte[] serializedRecord = readBlob(fin);
if (recordVersion < PLAINTEXT_VERSION && masterSecret != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
serializedRecord = masterCipher.decryptBytes(serializedRecord);
} else if (recordVersion < PLAINTEXT_VERSION) {
throw new AssertionError("Migration didn't happen!");
}
return serializedRecord;
}
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
MasterCipher masterCipher = new MasterCipher(masterSecret);
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
FileChannel out = recordFile.getChannel();
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
FileChannel out = recordFile.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeBlob(masterCipher.encryptBytes(serialized), out);
writeBlob(serialized, out);
out.truncate(out.position());
recordFile.close();
}

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
@@ -34,37 +36,46 @@ public class TextSecureSessionStore implements SessionStore {
private static final int SINGLE_STATE_VERSION = 1;
private static final int ARCHIVE_STATES_VERSION = 2;
private static final int CURRENT_VERSION = 2;
private static final int PLAINTEXT_VERSION = 3;
private static final int CURRENT_VERSION = 3;
private final Context context;
private final MasterSecret masterSecret;
@NonNull private final Context context;
@Nullable private final MasterSecret masterSecret;
public TextSecureSessionStore(Context context, MasterSecret masterSecret) {
public TextSecureSessionStore(@NonNull Context context) {
this(context, null);
}
public TextSecureSessionStore(@NonNull Context context, @Nullable MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
@Override
public SessionRecord loadSession(AxolotlAddress address) {
public SessionRecord loadSession(@NonNull AxolotlAddress address) {
synchronized (FILE_LOCK) {
try {
MasterCipher cipher = new MasterCipher(masterSecret);
FileInputStream in = new FileInputStream(getSessionFile(address));
int versionMarker = readInteger(in);
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));
byte[] serialized = readBlob(in);
in.close();
if (versionMarker < PLAINTEXT_VERSION && masterSecret != null) {
serialized = new MasterCipher(masterSecret).decryptBytes(serialized);
} else if (versionMarker < PLAINTEXT_VERSION) {
throw new AssertionError("Session didn't get migrated: (" + versionMarker + "," + address + ")");
}
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) {
} else if (versionMarker >= ARCHIVE_STATES_VERSION) {
return new SessionRecord(serialized);
} else {
throw new AssertionError("Unknown version: " + versionMarker);
@@ -77,16 +88,15 @@ public class TextSecureSessionStore implements SessionStore {
}
@Override
public void storeSession(AxolotlAddress address, SessionRecord record) {
public void storeSession(@NonNull AxolotlAddress address, @NonNull 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);
writeBlob(record.serialize(), out);
out.truncate(out.position());
sessionFile.close();
@@ -143,6 +153,23 @@ public class TextSecureSessionStore implements SessionStore {
return results;
}
public void migrateSessions() {
synchronized (FILE_LOCK) {
File directory = getSessionDirectory();
for (File session : directory.listFiles()) {
if (session.isFile()) {
AxolotlAddress address = getAddressName(session);
if (address != null) {
SessionRecord sessionRecord = loadSession(address);
storeSession(address, sessionRecord);
}
}
}
}
}
private File getSessionFile(AxolotlAddress address) {
return new File(getSessionDirectory(), getSessionName(address));
}
@@ -168,6 +195,23 @@ public class TextSecureSessionStore implements SessionStore {
return recipientId + (deviceId == TextSecureAddress.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
}
private @Nullable AxolotlAddress getAddressName(File sessionFile) {
try {
String[] parts = sessionFile.getName().split("[.]");
Recipient recipient = RecipientFactory.getRecipientForId(context, Integer.valueOf(parts[0]), true);
int deviceId;
if (parts.length > 1) deviceId = Integer.parseInt(parts[1]);
else deviceId = TextSecureAddress.DEFAULT_DEVICE_ID;
return new AxolotlAddress(recipient.getNumber(), deviceId);
} catch (NumberFormatException e) {
Log.w(TAG, e);
return null;
}
}
private byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];