Migrate prekeys into database
parent
6239508b39
commit
9f3c04dfb5
@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class OneTimePreKeyDatabase extends Database {
|
||||
|
||||
private static final String TAG = OneTimePreKeyDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "one_time_prekeys";
|
||||
private static final String ID = "_id";
|
||||
public static final String KEY_ID = "key_id";
|
||||
public static final String PUBLIC_KEY = "public_key";
|
||||
public static final String PRIVATE_KEY = "private_key";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
KEY_ID + " INTEGER UNIQUE, " +
|
||||
PUBLIC_KEY + " TEXT NOT NULL, " +
|
||||
PRIVATE_KEY + " TEXT NOT NULL);";
|
||||
|
||||
OneTimePreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public @Nullable PreKeyRecord getPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?",
|
||||
new String[] {String.valueOf(keyId)},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
|
||||
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void insertPreKey(int keyId, PreKeyRecord record) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(KEY_ID, keyId);
|
||||
contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize()));
|
||||
|
||||
database.insert(TABLE_NAME, null, contentValues);
|
||||
}
|
||||
|
||||
public void removePreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, KEY_ID + " = ?", new String[] {String.valueOf(keyId)});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SignedPreKeyDatabase extends Database {
|
||||
|
||||
private static final String TAG = SignedPreKeyDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "signed_prekeys";
|
||||
|
||||
private static final String ID = "_id";
|
||||
public static final String KEY_ID = "key_id";
|
||||
public static final String PUBLIC_KEY = "public_key";
|
||||
public static final String PRIVATE_KEY = "private_key";
|
||||
public static final String SIGNATURE = "signature";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
KEY_ID + " INTEGER UNIQUE, " +
|
||||
PUBLIC_KEY + " TEXT NOT NULL, " +
|
||||
PRIVATE_KEY + " TEXT NOT NULL, " +
|
||||
SIGNATURE + " TEXT NOT NULL, " +
|
||||
TIMESTAMP + " INTEGER DEFAULT 0);";
|
||||
|
||||
public SignedPreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public @Nullable SignedPreKeyRecord getSignedPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?",
|
||||
new String[] {String.valueOf(keyId)},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE)));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
|
||||
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<SignedPreKeyRecord> getAllSignedPreKeys() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SignedPreKeyRecord> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
int keyId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_ID));
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE)));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
|
||||
results.add(new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature));
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void insertSignedPreKey(int keyId, SignedPreKeyRecord record) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(KEY_ID, keyId);
|
||||
contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize()));
|
||||
contentValues.put(SIGNATURE, Base64.encodeBytes(record.getSignature()));
|
||||
contentValues.put(TIMESTAMP, record.getTimestamp());
|
||||
|
||||
database.insert(TABLE_NAME, null, contentValues);
|
||||
}
|
||||
|
||||
|
||||
public void removeSignedPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, KEY_ID + " = ? AND " + SIGNATURE + " IS NOT NULL", new String[] {String.valueOf(keyId)});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
class PreKeyMigrationHelper {
|
||||
|
||||
private static final String PREKEY_DIRECTORY = "prekeys";
|
||||
private static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
|
||||
|
||||
private static final int PLAINTEXT_VERSION = 2;
|
||||
private static final int CURRENT_VERSION_MARKER = 2;
|
||||
|
||||
private static final String TAG = PreKeyMigrationHelper.class.getSimpleName();
|
||||
|
||||
static boolean migratePreKeys(Context context, SQLiteDatabase database) {
|
||||
File[] preKeyFiles = getPreKeyDirectory(context).listFiles();
|
||||
boolean clean = true;
|
||||
|
||||
if (preKeyFiles != null) {
|
||||
for (File preKeyFile : preKeyFiles) {
|
||||
if (!"index.dat".equals(preKeyFile.getName())) {
|
||||
try {
|
||||
PreKeyRecord preKey = new PreKeyRecord(loadSerializedRecord(preKeyFile));
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(OneTimePreKeyDatabase.KEY_ID, preKey.getId());
|
||||
contentValues.put(OneTimePreKeyDatabase.PUBLIC_KEY, Base64.encodeBytes(preKey.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(OneTimePreKeyDatabase.PRIVATE_KEY, Base64.encodeBytes(preKey.getKeyPair().getPrivateKey().serialize()));
|
||||
database.insert(OneTimePreKeyDatabase.TABLE_NAME, null, contentValues);
|
||||
Log.w(TAG, "Migrated one-time prekey: " + preKey.getId());
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
clean = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File[] signedPreKeyFiles = getSignedPreKeyDirectory(context).listFiles();
|
||||
|
||||
if (signedPreKeyFiles != null) {
|
||||
for (File signedPreKeyFile : signedPreKeyFiles) {
|
||||
if (!"index.dat".equals(signedPreKeyFile.getName())) {
|
||||
try {
|
||||
SignedPreKeyRecord signedPreKey = new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile));
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SignedPreKeyDatabase.KEY_ID, signedPreKey.getId());
|
||||
contentValues.put(SignedPreKeyDatabase.PUBLIC_KEY, Base64.encodeBytes(signedPreKey.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(SignedPreKeyDatabase.PRIVATE_KEY, Base64.encodeBytes(signedPreKey.getKeyPair().getPrivateKey().serialize()));
|
||||
contentValues.put(SignedPreKeyDatabase.SIGNATURE, Base64.encodeBytes(signedPreKey.getSignature()));
|
||||
contentValues.put(SignedPreKeyDatabase.TIMESTAMP, signedPreKey.getTimestamp());
|
||||
database.insert(SignedPreKeyDatabase.TABLE_NAME, null, contentValues);
|
||||
Log.w(TAG, "Migrated signed prekey: " + signedPreKey.getId());
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
clean = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File oneTimePreKeyIndex = new File(getPreKeyDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
File signedPreKeyIndex = new File(getSignedPreKeyDirectory(context), SignedPreKeyIndex.FILE_NAME);
|
||||
|
||||
if (oneTimePreKeyIndex.exists()) {
|
||||
try {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(oneTimePreKeyIndex));
|
||||
PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class);
|
||||
reader.close();
|
||||
|
||||
Log.w(TAG, "Setting next prekey id: " + index.nextPreKeyId);
|
||||
TextSecurePreferences.setNextPreKeyId(context, index.nextPreKeyId);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPreKeyIndex.exists()) {
|
||||
try {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(signedPreKeyIndex));
|
||||
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
|
||||
reader.close();
|
||||
|
||||
Log.w(TAG, "Setting next signed prekey id: " + index.nextSignedPreKeyId);
|
||||
Log.w(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId);
|
||||
TextSecurePreferences.setNextSignedPreKeyId(context, index.nextSignedPreKeyId);
|
||||
TextSecurePreferences.setActiveSignedPreKeyId(context, index.activeSignedPreKeyId);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
static void cleanUpPreKeys(@NonNull Context context) {
|
||||
File preKeyDirectory = getPreKeyDirectory(context);
|
||||
File[] preKeyFiles = preKeyDirectory.listFiles();
|
||||
|
||||
if (preKeyFiles != null) {
|
||||
for (File preKeyFile : preKeyFiles) {
|
||||
Log.w(TAG, "Deleting: " + preKeyFile.getAbsolutePath());
|
||||
preKeyFile.delete();
|
||||
}
|
||||
|
||||
Log.w(TAG, "Deleting: " + preKeyDirectory.getAbsolutePath());
|
||||
preKeyDirectory.delete();
|
||||
}
|
||||
|
||||
File signedPreKeyDirectory = getSignedPreKeyDirectory(context);
|
||||
File[] signedPreKeyFiles = signedPreKeyDirectory.listFiles();
|
||||
|
||||
if (signedPreKeyFiles != null) {
|
||||
for (File signedPreKeyFile : signedPreKeyFiles) {
|
||||
Log.w(TAG, "Deleting: " + signedPreKeyFile.getAbsolutePath());
|
||||
signedPreKeyFile.delete();
|
||||
}
|
||||
|
||||
Log.w(TAG, "Deleting: " + signedPreKeyDirectory.getAbsolutePath());
|
||||
signedPreKeyDirectory.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] loadSerializedRecord(File recordFile)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
FileInputStream fin = new FileInputStream(recordFile);
|
||||
int recordVersion = readInteger(fin);
|
||||
|
||||
if (recordVersion > CURRENT_VERSION_MARKER) {
|
||||
throw new IOException("Invalid version: " + recordVersion);
|
||||
}
|
||||
|
||||
byte[] serializedRecord = readBlob(fin);
|
||||
|
||||
if (recordVersion < PLAINTEXT_VERSION) {
|
||||
throw new IOException("Migration didn't happen! " + recordFile.getAbsolutePath() + ", " + recordVersion);
|
||||
}
|
||||
|
||||
fin.close();
|
||||
return serializedRecord;
|
||||
}
|
||||
|
||||
private static File getPreKeyDirectory(Context context) {
|
||||
return getRecordsDirectory(context, PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getSignedPreKeyDirectory(Context context) {
|
||||
return getRecordsDirectory(context, SIGNED_PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getRecordsDirectory(Context context, String directoryName) {
|
||||
File directory = new File(context.getFilesDir(), directoryName);
|
||||
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.w(TAG, "PreKey directory creation failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private static int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
private static class PreKeyIndex {
|
||||
static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextPreKeyId;
|
||||
|
||||
public PreKeyIndex() {}
|
||||
}
|
||||
|
||||
private static class SignedPreKeyIndex {
|
||||
static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextSignedPreKeyId;
|
||||
|
||||
@JsonProperty
|
||||
private int activeSignedPreKeyId = -1;
|
||||
|
||||
public SignedPreKeyIndex() {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue