Support PNI prekeys.

This commit is contained in:
Greyson Parrelli
2022-02-01 14:09:04 -05:00
parent db534cd376
commit e8ad1e8ed1
32 changed files with 808 additions and 532 deletions

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.KeyValuePersistentStorage;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.io.File;
import java.util.Collection;
import java.util.Map;
@@ -60,6 +62,11 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
return instance;
}
public static boolean exists(Context context) {
return context.getDatabasePath(DATABASE_NAME).exists();
}
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook());

View File

@@ -1,79 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
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 = Log.tag(OneTimePreKeyDatabase.class);
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, SignalDatabase databaseHelper) {
super(context, databaseHelper);
}
public @Nullable PreKeyRecord getPreKey(int keyId) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
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.getSignalWritableDatabase();
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.replace(TABLE_NAME, null, contentValues);
}
public void removePreKey(int keyId) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
database.delete(TABLE_NAME, KEY_ID + " = ?", new String[] {String.valueOf(keyId)});
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import androidx.core.content.contentValuesOf
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.SqlUtil
import org.whispersystems.libsignal.InvalidKeyException
import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.state.PreKeyRecord
import org.whispersystems.signalservice.api.push.AccountIdentifier
import java.io.IOException
class OneTimePreKeyDatabase internal constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
companion object {
private val TAG = Log.tag(OneTimePreKeyDatabase::class.java)
const val TABLE_NAME = "one_time_prekeys"
const val ID = "_id"
const val ACCOUNT_ID = "account_id"
const val KEY_ID = "key_id"
const val PUBLIC_KEY = "public_key"
const val PRIVATE_KEY = "private_key"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$ACCOUNT_ID TEXT NOT NULL,
$KEY_ID INTEGER UNIQUE,
$PUBLIC_KEY TEXT NOT NULL,
$PRIVATE_KEY TEXT NOT NULL,
UNIQUE($ACCOUNT_ID, $KEY_ID)
)
"""
}
fun get(accountId: AccountIdentifier, keyId: Int): PreKeyRecord? {
readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
try {
val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0)
val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY)))
return PreKeyRecord(keyId, ECKeyPair(publicKey, privateKey))
} catch (e: InvalidKeyException) {
Log.w(TAG, e)
} catch (e: IOException) {
Log.w(TAG, e)
}
}
}
return null
}
fun insert(accountId: AccountIdentifier, keyId: Int, record: PreKeyRecord) {
val contentValues = contentValuesOf(
ACCOUNT_ID to accountId.toString(),
KEY_ID to keyId,
PUBLIC_KEY to Base64.encodeBytes(record.keyPair.publicKey.serialize()),
PRIVATE_KEY to Base64.encodeBytes(record.keyPair.privateKey.serialize())
)
writableDatabase.replace(TABLE_NAME, null, contentValues)
}
fun delete(accountId: AccountIdentifier, keyId: Int) {
val database = databaseHelper.signalWritableDatabase
database.delete(TABLE_NAME, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId))
}
}

View File

@@ -397,8 +397,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
get() = instance!!.pendingRetryReceiptDatabase
@get:JvmStatic
@get:JvmName("preKeys")
val preKeys: OneTimePreKeyDatabase
@get:JvmName("oneTimePreKeys")
val oneTimePreKeys: OneTimePreKeyDatabase
get() = instance!!.preKeyDatabase
@get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.")

View File

@@ -1,115 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
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 = Log.tag(SignedPreKeyDatabase.class);
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);";
SignedPreKeyDatabase(Context context, SignalDatabase databaseHelper) {
super(context, databaseHelper);
}
public @Nullable SignedPreKeyRecord getSignedPreKey(int keyId) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
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.getSignalReadableDatabase();
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.getSignalWritableDatabase();
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.replace(TABLE_NAME, null, contentValues);
}
public void removeSignedPreKey(int keyId) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
database.delete(TABLE_NAME, KEY_ID + " = ? AND " + SIGNATURE + " IS NOT NULL", new String[] {String.valueOf(keyId)});
}
}

View File

@@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import androidx.core.content.contentValuesOf
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.SqlUtil
import org.whispersystems.libsignal.InvalidKeyException
import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.state.SignedPreKeyRecord
import org.whispersystems.signalservice.api.push.AccountIdentifier
import java.io.IOException
import java.util.LinkedList
class SignedPreKeyDatabase internal constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
companion object {
private val TAG = Log.tag(SignedPreKeyDatabase::class.java)
const val TABLE_NAME = "signed_prekeys"
const val ID = "_id"
const val ACCOUNT_ID = "account_id"
const val KEY_ID = "key_id"
const val PUBLIC_KEY = "public_key"
const val PRIVATE_KEY = "private_key"
const val SIGNATURE = "signature"
const val TIMESTAMP = "timestamp"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$ACCOUNT_ID TEXT NOT NULL,
$KEY_ID INTEGER UNIQUE,
$PUBLIC_KEY TEXT NOT NULL,
$PRIVATE_KEY TEXT NOT NULL,
$SIGNATURE TEXT NOT NULL,
$TIMESTAMP INTEGER DEFAULT 0,
UNIQUE($ACCOUNT_ID, $KEY_ID)
)
"""
}
fun get(accountId: AccountIdentifier, keyId: Int): SignedPreKeyRecord? {
readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
try {
val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0)
val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY)))
val signature = Base64.decode(cursor.requireNonNullString(SIGNATURE))
val timestamp = cursor.requireLong(TIMESTAMP)
return SignedPreKeyRecord(keyId, timestamp, ECKeyPair(publicKey, privateKey), signature)
} catch (e: InvalidKeyException) {
Log.w(TAG, e)
} catch (e: IOException) {
Log.w(TAG, e)
}
}
}
return null
}
fun getAll(accountId: AccountIdentifier): List<SignedPreKeyRecord> {
val results: MutableList<SignedPreKeyRecord> = LinkedList()
readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ?", SqlUtil.buildArgs(accountId), null, null, null).use { cursor ->
while (cursor.moveToNext()) {
try {
val keyId = cursor.requireInt(KEY_ID)
val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0)
val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY)))
val signature = Base64.decode(cursor.requireNonNullString(SIGNATURE))
val timestamp = cursor.requireLong(TIMESTAMP)
results.add(SignedPreKeyRecord(keyId, timestamp, ECKeyPair(publicKey, privateKey), signature))
} catch (e: InvalidKeyException) {
Log.w(TAG, e)
} catch (e: IOException) {
Log.w(TAG, e)
}
}
}
return results
}
fun insert(accountId: AccountIdentifier, keyId: Int, record: SignedPreKeyRecord) {
val contentValues = contentValuesOf(
ACCOUNT_ID to accountId.toString(),
KEY_ID to keyId,
PUBLIC_KEY to Base64.encodeBytes(record.keyPair.publicKey.serialize()),
PRIVATE_KEY to Base64.encodeBytes(record.keyPair.privateKey.serialize()),
SIGNATURE to Base64.encodeBytes(record.signature),
TIMESTAMP to record.timestamp
)
writableDatabase.replace(TABLE_NAME, null, contentValues)
}
fun delete(accountId: AccountIdentifier, keyId: Int) {
writableDatabase.delete(TABLE_NAME, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId))
}
}

View File

@@ -14,6 +14,7 @@ import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -94,7 +95,7 @@ public final class PreKeyMigrationHelper {
reader.close();
Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId);
TextSecurePreferences.setNextPreKeyId(context, index.nextPreKeyId);
SignalStore.account().aciPreKeys().setNextOneTimePreKeyId(index.nextPreKeyId);
} catch (IOException e) {
Log.w(TAG, e);
}
@@ -108,8 +109,8 @@ public final class PreKeyMigrationHelper {
Log.i(TAG, "Setting next signed prekey id: " + index.nextSignedPreKeyId);
Log.i(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId);
TextSecurePreferences.setNextSignedPreKeyId(context, index.nextSignedPreKeyId);
TextSecurePreferences.setActiveSignedPreKeyId(context, index.activeSignedPreKeyId);
SignalStore.account().aciPreKeys().setNextSignedPreKeyId(index.nextSignedPreKeyId);
SignalStore.account().aciPreKeys().setActiveSignedPreKeyId(index.activeSignedPreKeyId);
} catch (IOException e) {
Log.w(TAG, e);
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database.helpers
import android.app.Application
import android.app.NotificationChannel
import android.content.ContentValues
import android.content.Context
@@ -18,8 +19,10 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
import org.thoughtcrime.securesms.database.requireString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.util.SqlUtil
import org.thoughtcrime.securesms.util.Stopwatch
import org.thoughtcrime.securesms.util.Triple
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.DistributionId
import java.io.ByteArrayInputStream
import java.io.File
@@ -185,11 +189,12 @@ object SignalDatabaseMigrations {
private const val PNI_CLEANUP = 127
private const val MESSAGE_RANGES = 128
private const val REACTION_TRIGGER_FIX = 129
private const val PNI_STORES = 130
const val DATABASE_VERSION = 129
const val DATABASE_VERSION = 130
@JvmStatic
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
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.id)
@@ -2285,6 +2290,80 @@ object SignalDatabaseMigrations {
""".trimIndent()
)
}
if (oldVersion < PNI_STORES) {
val localAci: ACI? = getLocalAci(context)
// One-Time Prekeys
db.execSQL(
"""
CREATE TABLE one_time_prekeys_tmp (
_id INTEGER PRIMARY KEY,
account_id TEXT NOT NULL,
key_id INTEGER,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
UNIQUE(account_id, key_id)
)
""".trimIndent()
)
if (localAci != null) {
db.execSQL(
"""
INSERT INTO one_time_prekeys_tmp (account_id, key_id, public_key, private_key)
SELECT
'$localAci' AS account_id,
one_time_prekeys.key_id,
one_time_prekeys.public_key,
one_time_prekeys.private_key
FROM one_time_prekeys
""".trimIndent()
)
} else {
Log.w(TAG, "No local ACI set. Not migrating any existing one-time prekeys.")
}
db.execSQL("DROP TABLE one_time_prekeys")
db.execSQL("ALTER TABLE one_time_prekeys_tmp RENAME TO one_time_prekeys")
// Signed Prekeys
db.execSQL(
"""
CREATE TABLE signed_prekeys_tmp (
_id INTEGER PRIMARY KEY,
account_id TEXT NOT NULL,
key_id INTEGER,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
signature TEXT NOT NULL,
timestamp INTEGER DEFAULT 0,
UNIQUE(account_id, key_id)
)
""".trimIndent()
)
if (localAci != null) {
db.execSQL(
"""
INSERT INTO signed_prekeys_tmp (account_id, key_id, public_key, private_key, signature, timestamp)
SELECT
'$localAci' AS account_id,
signed_prekeys.key_id,
signed_prekeys.public_key,
signed_prekeys.private_key,
signed_prekeys.signature,
signed_prekeys.timestamp
FROM signed_prekeys
""".trimIndent()
)
} else {
Log.w(TAG, "No local ACI set. Not migrating any existing signed prekeys.")
}
db.execSQL("DROP TABLE signed_prekeys")
db.execSQL("ALTER TABLE signed_prekeys_tmp RENAME TO signed_prekeys")
}
}
@JvmStatic
@@ -2294,6 +2373,9 @@ object SignalDatabaseMigrations {
}
}
/**
* Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method.
*/
private fun migrateReaction(db: SQLiteDatabase, cursor: Cursor, isMms: Boolean) {
try {
val messageId = CursorUtil.requireLong(cursor, "_id")
@@ -2314,4 +2396,22 @@ object SignalDatabaseMigrations {
Log.w(TAG, "Failed to parse reaction!")
}
}
/**
* Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method.
*/
private fun getLocalAci(context: Application): ACI? {
if (KeyValueDatabase.exists(context)) {
val keyValueDatabase = KeyValueDatabase.getInstance(context).readableDatabase
keyValueDatabase.query("key_value", arrayOf("value"), "key = ?", SqlUtil.buildArgs("account.aci"), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
ACI.parseOrNull(cursor.requireString("value"))
} else {
null
}
}
} else {
return ACI.parseOrNull(PreferenceManager.getDefaultSharedPreferences(context).getString("pref_local_uuid", null))
}
}
}