Migrate prekeys into database

This commit is contained in:
Moxie Marlinspike
2018-02-15 20:33:10 -08:00
parent 6239508b39
commit 9f3c04dfb5
10 changed files with 570 additions and 381 deletions

View File

@@ -39,20 +39,22 @@ public class DatabaseFactory {
private static DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final OneTimePreKeyDatabase preKeyDatabase;
private final SignedPreKeyDatabase signedPreKeyDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@@ -115,6 +117,14 @@ public class DatabaseFactory {
return getInstance(context).groupReceiptDatabase;
}
public static OneTimePreKeyDatabase getPreKeyDatabase(Context context) {
return getInstance(context).preKeyDatabase;
}
public static SignedPreKeyDatabase getSignedPreKeyDatabase(Context context) {
return getInstance(context).signedPreKeyDatabase;
}
private DatabaseFactory(@NonNull Context context) {
SQLiteDatabase.loadLibs(context);
@@ -135,6 +145,8 @@ public class DatabaseFactory {
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
@@ -35,6 +35,7 @@ import java.io.IOException;
public class IdentityDatabase extends Database {
@SuppressWarnings("unused")
private static final String TAG = IdentityDatabase.class.getSimpleName();
private static final String TABLE_NAME = "identities";
@@ -218,7 +219,7 @@ public class IdentityDatabase extends Database {
public class IdentityReader {
private final Cursor cursor;
public IdentityReader(@NonNull Cursor cursor) {
IdentityReader(@NonNull Cursor cursor) {
this.cursor = cursor;
}

View File

@@ -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)});
}
}

View File

@@ -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)});
}
}

View File

@@ -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() {}
}
}

View File

@@ -9,6 +9,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@@ -17,10 +18,13 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -29,9 +33,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@SuppressWarnings("unused")
private static final String TAG = SQLCipherOpenHelper.class.getSimpleName();
private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2;
private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2;
private static final int MIGRATE_PREKEYS_VERSION = 3;
private static final int DATABASE_VERSION = 2;
private static final int DATABASE_VERSION = 3;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -68,6 +73,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(GroupDatabase.CREATE_TABLE);
db.execSQL(RecipientDatabase.CREATE_TABLE);
db.execSQL(GroupReceiptDatabase.CREATE_TABLE);
db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE);
db.execSQL(SignedPreKeyDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@@ -87,6 +94,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null);
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
}
PreKeyMigrationHelper.cleanUpPreKeys(context);
}
}
@@ -94,9 +107,33 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database: " + oldVersion + ", " + newVersion);
if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId());
db.beginTransaction();
try {
if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId());
}
if (oldVersion < MIGRATE_PREKEYS_VERSION) {
db.execSQL("CREATE TABLE signed_prekeys (_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)");
db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)");
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
}
PreKeyMigrationHelper.cleanUpPreKeys(context);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (oldVersion < MIGRATE_PREKEYS_VERSION) {
PreKeyMigrationHelper.cleanUpPreKeys(context);
}
}