mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-26 09:59:28 +00:00
Only use MasterSecret for local message encryption.
Not for the axolotl store. // FREEBIE
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
29
src/org/thoughtcrime/securesms/crypto/MasterSecretUnion.java
Normal file
29
src/org/thoughtcrime/securesms/crypto/MasterSecretUnion.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
31
src/org/thoughtcrime/securesms/crypto/MediaKey.java
Normal file
31
src/org/thoughtcrime/securesms/crypto/MediaKey.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user