mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Add first cut of protocol v3 support.
1) Use the new /v2/keys API for storing/retrieving prekey bundles. 2) For sessions built with PreKeyBundle and PreKeyWhisperMessage, use a v3 ratcheting session when available.
This commit is contained in:
@@ -22,9 +22,14 @@ import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve25519;
|
||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.state.DeviceKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.DeviceKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.libaxolotl.util.Medium;
|
||||
@@ -36,7 +41,6 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -54,7 +58,7 @@ public class PreKeyUtil {
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair(true);
|
||||
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
|
||||
|
||||
preKeyStore.store(preKeyId, record);
|
||||
preKeyStore.storePreKey(preKeyId, record);
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
@@ -62,59 +66,45 @@ public class PreKeyUtil {
|
||||
return records;
|
||||
}
|
||||
|
||||
public static DeviceKeyRecord generateDeviceKey(Context context, MasterSecret masterSecret,
|
||||
IdentityKeyPair identityKeyPair)
|
||||
{
|
||||
try {
|
||||
DeviceKeyStore deviceKeyStore = new TextSecurePreKeyStore(context, masterSecret);
|
||||
int deviceKeyId = getNextDeviceKeyId(context);
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair(true);
|
||||
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
|
||||
DeviceKeyRecord record = new DeviceKeyRecord(deviceKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
|
||||
deviceKeyStore.storeDeviceKey(deviceKeyId, record);
|
||||
setNextDeviceKeyId(context, (deviceKeyId + 1) % Medium.MAX_VALUE);
|
||||
|
||||
return record;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
|
||||
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
|
||||
|
||||
if (preKeyStore.contains(Medium.MAX_VALUE)) {
|
||||
if (preKeyStore.containsPreKey(Medium.MAX_VALUE)) {
|
||||
try {
|
||||
return preKeyStore.load(Medium.MAX_VALUE);
|
||||
return preKeyStore.loadPreKey(Medium.MAX_VALUE);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
preKeyStore.remove(Medium.MAX_VALUE);
|
||||
preKeyStore.removePreKey(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair(true);
|
||||
PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
|
||||
|
||||
preKeyStore.store(Medium.MAX_VALUE, record);
|
||||
preKeyStore.storePreKey(Medium.MAX_VALUE, record);
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
// public static List<PreKeyRecord> getPreKeys(Context context, MasterSecret masterSecret) {
|
||||
// List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
// File directory = getPreKeysDirectory(context);
|
||||
// String[] keyRecordIds = directory.list();
|
||||
//
|
||||
// Arrays.sort(keyRecordIds, new PreKeyRecordIdComparator());
|
||||
//
|
||||
// for (String keyRecordId : keyRecordIds) {
|
||||
// try {
|
||||
// if (!keyRecordId.equals(PreKeyIndex.FILE_NAME) && Integer.parseInt(keyRecordId) != Medium.MAX_VALUE) {
|
||||
// records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId)));
|
||||
// }
|
||||
// } catch (InvalidKeyIdException e) {
|
||||
// Log.w("PreKeyUtil", e);
|
||||
// new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
// } catch (NumberFormatException nfe) {
|
||||
// Log.w("PreKeyUtil", nfe);
|
||||
// new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return records;
|
||||
// }
|
||||
//
|
||||
// public static void clearPreKeys(Context context) {
|
||||
// File directory = getPreKeysDirectory(context);
|
||||
// String[] keyRecords = directory.list();
|
||||
//
|
||||
// for (String keyRecord : keyRecords) {
|
||||
// new File(directory, keyRecord).delete();
|
||||
// }
|
||||
// }
|
||||
|
||||
private static void setNextPreKeyId(Context context, int id) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
@@ -126,6 +116,17 @@ public class PreKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNextDeviceKeyId(Context context, int id) {
|
||||
try {
|
||||
File nextFile = new File(getDeviceKeysDirectory(context), DeviceKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new DeviceKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextPreKeyId(Context context) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
@@ -144,8 +145,34 @@ public class PreKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextDeviceKeyId(Context context) {
|
||||
try {
|
||||
File nextFile = new File(getDeviceKeysDirectory(context), DeviceKeyIndex.FILE_NAME);
|
||||
|
||||
if (!nextFile.exists()) {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
DeviceKeyIndex index = new Gson().fromJson(reader, DeviceKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextDeviceKeyId;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getPreKeysDirectory(Context context) {
|
||||
File directory = new File(context.getFilesDir(), TextSecurePreKeyStore.PREKEY_DIRECTORY);
|
||||
return getKeysDirectory(context, TextSecurePreKeyStore.PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getDeviceKeysDirectory(Context context) {
|
||||
return getKeysDirectory(context, TextSecurePreKeyStore.DEVICE_KEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getKeysDirectory(Context context, String name) {
|
||||
File directory = new File(context.getFilesDir(), name);
|
||||
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
@@ -153,25 +180,6 @@ public class PreKeyUtil {
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static class PreKeyRecordIdComparator implements Comparator<String> {
|
||||
@Override
|
||||
public int compare(String lhs, String rhs) {
|
||||
if (lhs.equals(PreKeyIndex.FILE_NAME)) return -1;
|
||||
else if (rhs.equals(PreKeyIndex.FILE_NAME)) return 1;
|
||||
|
||||
try {
|
||||
long lhsLong = Long.parseLong(lhs);
|
||||
long rhsLong = Long.parseLong(rhs);
|
||||
|
||||
if (lhsLong < rhsLong) return -1;
|
||||
else if (lhsLong > rhsLong) return 1;
|
||||
else return 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
@@ -184,4 +192,17 @@ public class PreKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
private int nextDeviceKeyId;
|
||||
|
||||
public DeviceKeyIndex() {}
|
||||
|
||||
public DeviceKeyIndex(int nextDeviceKeyId) {
|
||||
this.nextDeviceKeyId = nextDeviceKeyId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class SessionCipherFactory {
|
||||
{
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||
|
||||
if (sessionStore.contains(recipient.getRecipientId(), recipient.getDeviceId())) {
|
||||
if (sessionStore.containsSession(recipient.getRecipientId(), recipient.getDeviceId())) {
|
||||
return new SessionCipher(sessionStore, recipient.getRecipientId(), recipient.getDeviceId());
|
||||
} else {
|
||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.GsonBuilder;
|
||||
import com.google.thoughtcrimegson.JsonDeserializationContext;
|
||||
import com.google.thoughtcrimegson.JsonDeserializer;
|
||||
import com.google.thoughtcrimegson.JsonElement;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.JsonPrimitive;
|
||||
import com.google.thoughtcrimegson.JsonSerializationContext;
|
||||
import com.google.thoughtcrimegson.JsonSerializer;
|
||||
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class DeviceKeyEntity extends PreKeyEntity {
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public DeviceKeyEntity() {}
|
||||
|
||||
public DeviceKeyEntity(int keyId, ECPublicKey publicKey, byte[] signature) {
|
||||
super(keyId, publicKey);
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static GsonBuilder forBuilder(GsonBuilder builder) {
|
||||
return PreKeyEntity.forBuilder(builder)
|
||||
.registerTypeAdapter(byte[].class, new ByteArrayJsonAdapter());
|
||||
|
||||
}
|
||||
|
||||
private static class ByteArrayJsonAdapter
|
||||
implements JsonSerializer<byte[]>, JsonDeserializer<byte[]>
|
||||
{
|
||||
@Override
|
||||
public JsonElement serialize(byte[] signature, Type type,
|
||||
JsonSerializationContext jsonSerializationContext)
|
||||
{
|
||||
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(signature));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] deserialize(JsonElement jsonElement, Type type,
|
||||
JsonDeserializationContext jsonDeserializationContext)
|
||||
throws JsonParseException
|
||||
{
|
||||
try {
|
||||
return Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString());
|
||||
} catch (IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,38 +8,26 @@ import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.JsonPrimitive;
|
||||
import com.google.thoughtcrimegson.JsonSerializationContext;
|
||||
import com.google.thoughtcrimegson.JsonSerializer;
|
||||
import com.google.thoughtcrimegson.annotations.Expose;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.libaxolotl.state.PreKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class PreKeyEntity implements PreKey {
|
||||
|
||||
@Expose(serialize = false)
|
||||
private int deviceId;
|
||||
|
||||
@Expose(serialize = false)
|
||||
private int registrationId;
|
||||
public class PreKeyEntity {
|
||||
|
||||
private int keyId;
|
||||
private ECPublicKey publicKey;
|
||||
private IdentityKey identityKey;
|
||||
|
||||
public PreKeyEntity(int keyId, ECPublicKey publicKey, IdentityKey identityKey) {
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
public PreKeyEntity() {}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
public PreKeyEntity(int keyId, ECPublicKey publicKey) {
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public int getKeyId() {
|
||||
@@ -50,28 +38,8 @@ public class PreKeyEntity implements PreKey {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public static String toJson(PreKeyEntity entity) {
|
||||
return getBuilder().create().toJson(entity);
|
||||
}
|
||||
|
||||
public static PreKeyEntity fromJson(String encoded) {
|
||||
return getBuilder().create().fromJson(encoded, PreKeyEntity.class);
|
||||
}
|
||||
|
||||
public static GsonBuilder getBuilder() {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
|
||||
builder.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter());
|
||||
|
||||
return builder;
|
||||
public static GsonBuilder forBuilder(GsonBuilder builder) {
|
||||
return builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
|
||||
}
|
||||
|
||||
|
||||
@@ -92,34 +60,7 @@ public class PreKeyEntity implements PreKey {
|
||||
{
|
||||
try {
|
||||
return Curve.decodePoint(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new JsonParseException(e);
|
||||
} catch (IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class IdentityKeyJsonAdapter
|
||||
implements JsonSerializer<IdentityKey>, JsonDeserializer<IdentityKey>
|
||||
{
|
||||
@Override
|
||||
public JsonElement serialize(IdentityKey identityKey, Type type,
|
||||
JsonSerializationContext jsonSerializationContext)
|
||||
{
|
||||
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey deserialize(JsonElement jsonElement, Type type,
|
||||
JsonDeserializationContext jsonDeserializationContext)
|
||||
throws JsonParseException
|
||||
{
|
||||
try {
|
||||
return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new JsonParseException(e);
|
||||
} catch (IOException e) {
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyList {
|
||||
|
||||
private PreKeyEntity lastResortKey;
|
||||
private List<PreKeyEntity> keys;
|
||||
|
||||
public PreKeyList(PreKeyEntity lastResortKey, List<PreKeyEntity> keys) {
|
||||
this.keys = keys;
|
||||
this.lastResortKey = lastResortKey;
|
||||
}
|
||||
|
||||
public List<PreKeyEntity> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public static String toJson(PreKeyList entity) {
|
||||
return PreKeyEntity.getBuilder().create().toJson(entity);
|
||||
}
|
||||
|
||||
public static PreKeyList fromJson(String serialized) {
|
||||
return PreKeyEntity.getBuilder().create().fromJson(serialized, PreKeyList.class);
|
||||
}
|
||||
|
||||
public PreKeyEntity getLastResortKey() {
|
||||
return lastResortKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.GsonBuilder;
|
||||
import com.google.thoughtcrimegson.JsonDeserializationContext;
|
||||
import com.google.thoughtcrimegson.JsonDeserializer;
|
||||
import com.google.thoughtcrimegson.JsonElement;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.JsonPrimitive;
|
||||
import com.google.thoughtcrimegson.JsonSerializationContext;
|
||||
import com.google.thoughtcrimegson.JsonSerializer;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyResponse {
|
||||
|
||||
private IdentityKey identityKey;
|
||||
private List<PreKeyResponseItem> devices;
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public List<PreKeyResponseItem> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public static PreKeyResponse fromJson(String serialized) {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
return PreKeyResponseItem.forBuilder(builder)
|
||||
.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter())
|
||||
.create().fromJson(serialized, PreKeyResponse.class);
|
||||
}
|
||||
|
||||
public static class IdentityKeyJsonAdapter
|
||||
implements JsonSerializer<IdentityKey>, JsonDeserializer<IdentityKey>
|
||||
{
|
||||
@Override
|
||||
public JsonElement serialize(IdentityKey identityKey, Type type,
|
||||
JsonSerializationContext jsonSerializationContext)
|
||||
{
|
||||
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey deserialize(JsonElement jsonElement, Type type,
|
||||
JsonDeserializationContext jsonDeserializationContext)
|
||||
throws JsonParseException
|
||||
{
|
||||
try {
|
||||
return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.GsonBuilder;
|
||||
|
||||
public class PreKeyResponseItem {
|
||||
|
||||
private int deviceId;
|
||||
private int registrationId;
|
||||
private DeviceKeyEntity deviceKey;
|
||||
private PreKeyEntity preKey;
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public DeviceKeyEntity getDeviceKey() {
|
||||
return deviceKey;
|
||||
}
|
||||
|
||||
public PreKeyEntity getPreKey() {
|
||||
return preKey;
|
||||
}
|
||||
|
||||
public static GsonBuilder forBuilder(GsonBuilder builder) {
|
||||
return DeviceKeyEntity.forBuilder(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.GsonBuilder;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyState {
|
||||
|
||||
private IdentityKey identityKey;
|
||||
private List<PreKeyEntity> preKeys;
|
||||
private PreKeyEntity lastResortKey;
|
||||
private DeviceKeyEntity deviceKey;
|
||||
|
||||
|
||||
public PreKeyState(List<PreKeyEntity> preKeys, PreKeyEntity lastResortKey,
|
||||
DeviceKeyEntity deviceKey, IdentityKey identityKey)
|
||||
{
|
||||
this.preKeys = preKeys;
|
||||
this.lastResortKey = lastResortKey;
|
||||
this.deviceKey = deviceKey;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public static String toJson(PreKeyState state) {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
return DeviceKeyEntity.forBuilder(builder)
|
||||
.registerTypeAdapter(IdentityKey.class, new PreKeyResponse.IdentityKeyJsonAdapter())
|
||||
.create().toJson(state);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,9 @@ import com.google.thoughtcrimegson.JsonParseException;
|
||||
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.libaxolotl.state.DeviceKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyBundle;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.BlacklistingTrustManager;
|
||||
@@ -64,9 +67,9 @@ public class PushServiceSocket {
|
||||
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
|
||||
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
|
||||
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
|
||||
private static final String PREKEY_METADATA_PATH = "/v1/keys/";
|
||||
private static final String PREKEY_PATH = "/v1/keys/%s";
|
||||
private static final String PREKEY_DEVICE_PATH = "/v1/keys/%s/%s";
|
||||
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
|
||||
private static final String PREKEY_PATH = "/v2/keys/%s";
|
||||
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s";
|
||||
|
||||
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
|
||||
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
|
||||
@@ -126,6 +129,7 @@ public class PushServiceSocket {
|
||||
|
||||
public void registerPreKeys(IdentityKey identityKey,
|
||||
PreKeyRecord lastResortKey,
|
||||
DeviceKeyRecord deviceKey,
|
||||
List<PreKeyRecord> records)
|
||||
throws IOException
|
||||
{
|
||||
@@ -133,19 +137,20 @@ public class PushServiceSocket {
|
||||
|
||||
for (PreKeyRecord record : records) {
|
||||
PreKeyEntity entity = new PreKeyEntity(record.getId(),
|
||||
record.getKeyPair().getPublicKey(),
|
||||
identityKey);
|
||||
record.getKeyPair().getPublicKey());
|
||||
|
||||
entities.add(entity);
|
||||
}
|
||||
|
||||
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
|
||||
lastResortKey.getKeyPair().getPublicKey(),
|
||||
identityKey);
|
||||
lastResortKey.getKeyPair().getPublicKey());
|
||||
|
||||
DeviceKeyEntity deviceKeyEntity = new DeviceKeyEntity(deviceKey.getId(),
|
||||
deviceKey.getKeyPair().getPublicKey(),
|
||||
deviceKey.getSignature());
|
||||
|
||||
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
|
||||
PreKeyList.toJson(new PreKeyList(lastResortEntity, entities)));
|
||||
PreKeyState.toJson(new PreKeyState(entities, lastResortEntity, deviceKeyEntity, identityKey)));
|
||||
}
|
||||
|
||||
public int getAvailablePreKeys() throws IOException {
|
||||
@@ -155,7 +160,7 @@ public class PushServiceSocket {
|
||||
return preKeyStatus.getCount();
|
||||
}
|
||||
|
||||
public List<PreKeyEntity> getPreKeys(PushAddress destination) throws IOException {
|
||||
public List<PreKeyBundle> getPreKeys(PushAddress destination) throws IOException {
|
||||
try {
|
||||
String deviceId = String.valueOf(destination.getDeviceId());
|
||||
|
||||
@@ -168,10 +173,34 @@ public class PushServiceSocket {
|
||||
path = path + "?relay=" + destination.getRelay();
|
||||
}
|
||||
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyList response = PreKeyList.fromJson(responseText);
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
|
||||
List<PreKeyBundle> bundles = new LinkedList<>();
|
||||
|
||||
return response.getKeys();
|
||||
for (PreKeyResponseItem device : response.getDevices()) {
|
||||
ECPublicKey preKey = null;
|
||||
ECPublicKey deviceKey = null;
|
||||
byte[] deviceKeySignature = null;
|
||||
int preKeyId = -1;
|
||||
int deviceKeyId = -1;
|
||||
|
||||
if (device.getDeviceKey() != null) {
|
||||
deviceKey = device.getDeviceKey().getPublicKey();
|
||||
deviceKeyId = device.getDeviceKey().getKeyId();
|
||||
deviceKeySignature = device.getDeviceKey().getSignature();
|
||||
}
|
||||
|
||||
if (device.getPreKey() != null) {
|
||||
preKeyId = device.getPreKey().getKeyId();
|
||||
preKey = device.getPreKey().getPublicKey();
|
||||
}
|
||||
|
||||
bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId,
|
||||
preKey, deviceKeyId, deviceKey, deviceKeySignature,
|
||||
response.getIdentityKey()));
|
||||
}
|
||||
|
||||
return bundles;
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NotFoundException nfe) {
|
||||
@@ -179,7 +208,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyEntity getPreKey(PushAddress destination) throws IOException {
|
||||
public PreKeyBundle getPreKey(PushAddress destination) throws IOException {
|
||||
try {
|
||||
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
|
||||
String.valueOf(destination.getDeviceId()));
|
||||
@@ -188,13 +217,32 @@ public class PushServiceSocket {
|
||||
path = path + "?relay=" + destination.getRelay();
|
||||
}
|
||||
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyList response = PreKeyList.fromJson(responseText);
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
|
||||
|
||||
if (response.getKeys() == null || response.getKeys().size() < 1)
|
||||
if (response.getDevices() == null || response.getDevices().size() < 1)
|
||||
throw new IOException("Empty prekey list");
|
||||
|
||||
return response.getKeys().get(0);
|
||||
PreKeyResponseItem device = response.getDevices().get(0);
|
||||
ECPublicKey preKey = null;
|
||||
ECPublicKey deviceKey = null;
|
||||
byte[] deviceKeySignature = null;
|
||||
int preKeyId = -1;
|
||||
int deviceKeyId = -1;
|
||||
|
||||
if (device.getPreKey() != null) {
|
||||
preKeyId = device.getPreKey().getKeyId();
|
||||
preKey = device.getPreKey().getPublicKey();
|
||||
}
|
||||
|
||||
if (device.getDeviceKey() != null) {
|
||||
deviceKeyId = device.getDeviceKey().getKeyId();
|
||||
deviceKey = device.getDeviceKey().getPublicKey();
|
||||
deviceKeySignature = device.getDeviceKey().getSignature();
|
||||
}
|
||||
|
||||
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
|
||||
deviceKeyId, deviceKey, deviceKeySignature, response.getIdentityKey());
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NotFoundException nfe) {
|
||||
|
||||
@@ -25,8 +25,8 @@ public class SessionUtil {
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||
|
||||
return
|
||||
sessionStore.contains(recipientId, deviceId) &&
|
||||
!sessionStore.load(recipientId, deviceId).getSessionState().getNeedsRefresh();
|
||||
sessionStore.containsSession(recipientId, deviceId) &&
|
||||
!sessionStore.loadSession(recipientId, deviceId).getSessionState().getNeedsRefresh();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.state.DeviceKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.DeviceKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
@@ -17,10 +19,15 @@ 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 TextSecurePreKeyStore implements PreKeyStore, DeviceKeyStore {
|
||||
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
public static final String DEVICE_KEY_DIRECTORY = "device_keys";
|
||||
|
||||
public class TextSecurePreKeyStore implements PreKeyStore {
|
||||
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
private static final int CURRENT_VERSION_MARKER = 1;
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
|
||||
@@ -34,20 +41,10 @@ public class TextSecurePreKeyStore implements PreKeyStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord load(int preKeyId) throws InvalidKeyIdException {
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
FileInputStream fin = new FileInputStream(getPreKeyFile(preKeyId));
|
||||
int recordVersion = readInteger(fin);
|
||||
|
||||
if (recordVersion != CURRENT_VERSION_MARKER) {
|
||||
throw new AssertionError("Invalid version: " + recordVersion);
|
||||
}
|
||||
|
||||
byte[] serializedRecord = masterCipher.decryptBytes(readBlob(fin));
|
||||
return new PreKeyRecord(serializedRecord);
|
||||
|
||||
return new PreKeyRecord(loadSerializedRecord(getPreKeyFile(preKeyId)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
@@ -56,19 +53,40 @@ public class TextSecurePreKeyStore implements PreKeyStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(int preKeyId, PreKeyRecord record) {
|
||||
public DeviceKeyRecord loadDeviceKey(int deviceKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
RandomAccessFile recordFile = new RandomAccessFile(getPreKeyFile(preKeyId), "rw");
|
||||
FileChannel out = recordFile.getChannel();
|
||||
return new DeviceKeyRecord(loadSerializedRecord(getDeviceKeyFile(deviceKeyId)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.position(0);
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeBlob(masterCipher.encryptBytes(record.serialize()), out);
|
||||
out.truncate(out.position());
|
||||
@Override
|
||||
public List<DeviceKeyRecord> loadDeviceKeys() {
|
||||
synchronized (FILE_LOCK) {
|
||||
File directory = getDeviceKeyDirectory();
|
||||
List<DeviceKeyRecord> results = new LinkedList<>();
|
||||
|
||||
recordFile.close();
|
||||
for (File deviceKeyFile : directory.listFiles()) {
|
||||
try {
|
||||
results.add(new DeviceKeyRecord(loadSerializedRecord(deviceKeyFile)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePreKey(int preKeyId, PreKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
storeSerializedRecord(getPreKeyFile(preKeyId), record.serialize());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
@@ -76,23 +94,85 @@ public class TextSecurePreKeyStore implements PreKeyStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(int preKeyId) {
|
||||
public void storeDeviceKey(int deviceKeyId, DeviceKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
storeSerializedRecord(getDeviceKeyFile(deviceKeyId), record.serialize());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPreKey(int preKeyId) {
|
||||
File record = getPreKeyFile(preKeyId);
|
||||
return record.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(int preKeyId) {
|
||||
public boolean containsDeviceKey(int deviceKeyId) {
|
||||
File record = getDeviceKeyFile(deviceKeyId);
|
||||
return record.exists();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void removePreKey(int preKeyId) {
|
||||
File record = getPreKeyFile(preKeyId);
|
||||
record.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeDeviceKey(int deviceKeyId) {
|
||||
File record = getDeviceKeyFile(deviceKeyId);
|
||||
record.delete();
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new AssertionError("Invalid version: " + recordVersion);
|
||||
}
|
||||
|
||||
return masterCipher.decryptBytes(readBlob(fin));
|
||||
}
|
||||
|
||||
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
|
||||
FileChannel out = recordFile.getChannel();
|
||||
|
||||
out.position(0);
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeBlob(masterCipher.encryptBytes(serialized), out);
|
||||
out.truncate(out.position());
|
||||
recordFile.close();
|
||||
}
|
||||
|
||||
private File getPreKeyFile(int preKeyId) {
|
||||
return new File(getPreKeyDirectory(), String.valueOf(preKeyId));
|
||||
}
|
||||
|
||||
private File getDeviceKeyFile(int deviceKeyId) {
|
||||
return new File(getDeviceKeyDirectory(), String.valueOf(deviceKeyId));
|
||||
}
|
||||
|
||||
private File getPreKeyDirectory() {
|
||||
File directory = new File(context.getFilesDir(), PREKEY_DIRECTORY);
|
||||
return getRecordsDirectory(PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private File getDeviceKeyDirectory() {
|
||||
return getRecordsDirectory(DEVICE_KEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private File getRecordsDirectory(String directoryName) {
|
||||
File directory = new File(context.getFilesDir(), directoryName);
|
||||
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
@@ -127,4 +207,6 @@ public class TextSecurePreKeyStore implements PreKeyStore {
|
||||
out.write(ByteBuffer.wrap(valueBytes));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class TextSecureSessionStore implements SessionStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionRecord load(long recipientId, int deviceId) {
|
||||
public SessionRecord loadSession(long recipientId, int deviceId) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher cipher = new MasterCipher(masterSecret);
|
||||
@@ -73,7 +73,7 @@ public class TextSecureSessionStore implements SessionStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(long recipientId, int deviceId, SessionRecord record) {
|
||||
public void storeSession(long recipientId, int deviceId, SessionRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
@@ -93,24 +93,24 @@ public class TextSecureSessionStore implements SessionStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(long recipientId, int deviceId) {
|
||||
public boolean containsSession(long recipientId, int deviceId) {
|
||||
return getSessionFile(recipientId, deviceId).exists() &&
|
||||
load(recipientId, deviceId).getSessionState().hasSenderChain();
|
||||
loadSession(recipientId, deviceId).getSessionState().hasSenderChain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(long recipientId, int deviceId) {
|
||||
public void deleteSession(long recipientId, int deviceId) {
|
||||
getSessionFile(recipientId, deviceId).delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(long recipientId) {
|
||||
public void deleteAllSessions(long recipientId) {
|
||||
List<Integer> devices = getSubDeviceSessions(recipientId);
|
||||
|
||||
delete(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
|
||||
deleteSession(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
|
||||
|
||||
for (int device : devices) {
|
||||
delete(recipientId, device);
|
||||
deleteSession(recipientId, device);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user