mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 16:49:40 +01:00
Reject last-use kyber key sets that we've seen before.
This commit is contained in:
@@ -7,12 +7,15 @@ import org.signal.core.util.exists
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
|
||||
@@ -116,11 +119,33 @@ class KyberPreKeyTable(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
.run(SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
fun deleteIfNotLastResort(serviceId: ServiceId, keyId: Int) {
|
||||
writableDatabase
|
||||
.delete("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
|
||||
.where("$ACCOUNT_ID = ? AND $KEY_ID = ? AND $LAST_RESORT = ?", serviceId.toAccountId(), keyId, 0)
|
||||
.run()
|
||||
/**
|
||||
* When we mark Kyber pre-keys used, we want to keep a record of last resort tuples, which are deleted when they key
|
||||
* itself is deleted from this table via a cascading delete.
|
||||
*
|
||||
* For non-last-resort keys, this method just deletes them like normal.
|
||||
*/
|
||||
fun handleMarkKyberPreKeyUsed(serviceId: ServiceId, kyberPreKeyId: Int, signedPreKeyId: Int, baseKey: ECPublicKey) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val lastResortRowId = db
|
||||
.select(ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ACCOUNT_ID = ? AND $KEY_ID = ? AND $LAST_RESORT = ?", serviceId.toAccountId(), kyberPreKeyId, 1)
|
||||
.run()
|
||||
.readToSingleInt(-1)
|
||||
|
||||
if (lastResortRowId < 0) {
|
||||
db.delete("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
|
||||
.where("$ACCOUNT_ID = ? AND $KEY_ID = ? AND $LAST_RESORT = ?", serviceId.toAccountId(), kyberPreKeyId, 0)
|
||||
.run()
|
||||
} else {
|
||||
SignalDatabase.lastResortKeyTuples.insert(
|
||||
kyberPreKeyRowId = lastResortRowId,
|
||||
signedKeyId = signedPreKeyId,
|
||||
publicKey = baseKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(serviceId: ServiceId, keyId: Int) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.ReusedBaseKeyException
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
|
||||
/**
|
||||
* Stores a Tuple of (kyberPreKeyId, signedPreKeyId, baseKey) for each
|
||||
* last-resort key when [KyberPreKeyStore#markKyberPreKeyUsed] is called.
|
||||
*
|
||||
* Entries in this table are unique and an error will be thrown on trying to insert a duplicate.
|
||||
*/
|
||||
class LastResortKeyTupleTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(LastResortKeyTupleTable::class)
|
||||
|
||||
private const val TABLE_NAME = "last_resort_key_tuple"
|
||||
|
||||
private const val ID = "_id"
|
||||
private const val KYBER_PREKEY = "kyber_prekey_id"
|
||||
private const val SIGNED_KEY_ID = "signed_key_id"
|
||||
private const val PUBLIC_KEY = "public_key"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$KYBER_PREKEY INTEGER NOT NULL UNIQUE REFERENCES ${KyberPreKeyTable.TABLE_NAME} (${KyberPreKeyTable.ID}) ON DELETE CASCADE,
|
||||
$SIGNED_KEY_ID INTEGER NOT NULL,
|
||||
$PUBLIC_KEY BLOB NOT NULL,
|
||||
UNIQUE($KYBER_PREKEY, $SIGNED_KEY_ID, $PUBLIC_KEY)
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the Last-resort tuple. If it already exists, we will throw a [ReusedBaseKeyException]
|
||||
*/
|
||||
@Throws(ReusedBaseKeyException::class)
|
||||
fun insert(kyberPreKeyRowId: Int, signedKeyId: Int, publicKey: ECPublicKey) {
|
||||
try {
|
||||
writableDatabase.insertInto(TABLE_NAME)
|
||||
.values(
|
||||
KYBER_PREKEY to kyberPreKeyRowId,
|
||||
SIGNED_KEY_ID to signedKeyId,
|
||||
PUBLIC_KEY to publicKey.serialize()
|
||||
)
|
||||
.run(conflictStrategy = SQLiteDatabase.CONFLICT_ABORT)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
Log.w(TAG, "Found duplicate use of last-resort kyber key set.", e)
|
||||
throw ReusedBaseKeyException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val chatFoldersTable: ChatFolderTables = ChatFolderTables(context, this)
|
||||
val backupMediaSnapshotTable: BackupMediaSnapshotTable = BackupMediaSnapshotTable(context, this)
|
||||
val pollTable: PollTables = PollTables(context, this)
|
||||
val lastResortKeyTuples: LastResortKeyTupleTable = LastResortKeyTupleTable(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
@@ -150,6 +151,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, ChatFolderTables.CREATE_TABLE)
|
||||
executeStatements(db, PollTables.CREATE_TABLE)
|
||||
db.execSQL(BackupMediaSnapshotTable.CREATE_TABLE)
|
||||
db.execSQL(LastResortKeyTupleTable.CREATE_TABLE)
|
||||
|
||||
executeStatements(db, RecipientTable.CREATE_INDEXS)
|
||||
executeStatements(db, MessageTable.CREATE_INDEXS)
|
||||
@@ -590,5 +592,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
@get:JvmName("polls")
|
||||
val polls: PollTables
|
||||
get() = instance!!.pollTable
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("lastResortKeyTuples")
|
||||
val lastResortKeyTuples: LastResortKeyTupleTable
|
||||
get() = instance!!.lastResortKeyTuples
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V289_AddQuoteTarget
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V290_AddArchiveThumbnailTransferStateColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V291_NullOutRemoteKeyIfEmpty
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V292_AddPollTables
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V293_LastResortKeyTupleTableMigration
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -299,10 +300,11 @@ object SignalDatabaseMigrations {
|
||||
289 to V289_AddQuoteTargetContentTypeColumn,
|
||||
290 to V290_AddArchiveThumbnailTransferStateColumn,
|
||||
291 to V291_NullOutRemoteKeyIfEmpty,
|
||||
292 to V292_AddPollTables
|
||||
292 to V292_AddPollTables,
|
||||
293 to V293_LastResortKeyTupleTableMigration
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 292
|
||||
const val DATABASE_VERSION = 293
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Creates the LastResortKeyTuple table.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V293_LastResortKeyTupleTableMigration : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE last_resort_key_tuple (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
kyber_prekey_id INTEGER NOT NULL UNIQUE REFERENCES kyber_prekey (_id) ON DELETE CASCADE,
|
||||
signed_key_id INTEGER NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
UNIQUE(kyber_prekey_id, signed_key_id, public_key)
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user