That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.Context
4import org.signal.core.util.delete
5import org.signal.core.util.deleteAll
6import org.signal.core.util.exists
7import org.signal.core.util.insertInto
8import org.signal.core.util.logging.Log
9import org.signal.core.util.readToList
10import org.signal.core.util.readToSingleObject
11import org.signal.core.util.requireBoolean
12import org.signal.core.util.requireNonNullBlob
13import org.signal.core.util.select
14import org.signal.core.util.toInt
15import org.signal.core.util.update
16import org.signal.libsignal.protocol.state.KyberPreKeyRecord
17import org.whispersystems.signalservice.api.push.ServiceId
18
19/**
20 * A table for storing data related to [org.tm.archive.crypto.storage.SignalKyberPreKeyStore].
21 */
22class KyberPreKeyTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
23 companion object {
24 private val TAG = Log.tag(KyberPreKeyTable::class.java)
25
26 const val TABLE_NAME = "kyber_prekey"
27 const val ID = "_id"
28 const val ACCOUNT_ID = "account_id"
29 const val KEY_ID = "key_id"
30 const val TIMESTAMP = "timestamp"
31 const val LAST_RESORT = "last_resort"
32 const val SERIALIZED = "serialized"
33 const val STALE_TIMESTAMP = "stale_timestamp"
34
35 const val CREATE_TABLE = """
36 CREATE TABLE $TABLE_NAME (
37 $ID INTEGER PRIMARY KEY,
38 $ACCOUNT_ID TEXT NOT NULL,
39 $KEY_ID INTEGER NOT NULL,
40 $TIMESTAMP INTEGER NOT NULL,
41 $LAST_RESORT INTEGER NOT NULL,
42 $SERIALIZED BLOB NOT NULL,
43 $STALE_TIMESTAMP INTEGER NOT NULL DEFAULT 0,
44 UNIQUE($ACCOUNT_ID, $KEY_ID)
45 )
46 """
47
48 private const val INDEX_ACCOUNT_KEY = "kyber_account_id_key_id"
49
50 val CREATE_INDEXES = arrayOf(
51 "CREATE INDEX IF NOT EXISTS $INDEX_ACCOUNT_KEY ON $TABLE_NAME ($ACCOUNT_ID, $KEY_ID, $LAST_RESORT, $SERIALIZED)"
52 )
53
54 const val PNI_ACCOUNT_ID = "PNI"
55 }
56
57 fun get(serviceId: ServiceId, keyId: Int): KyberPreKey? {
58 return readableDatabase
59 .select(LAST_RESORT, SERIALIZED)
60 .from("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
61 .where("$ACCOUNT_ID = ? AND $KEY_ID = ?", serviceId.toAccountId(), keyId)
62 .run()
63 .readToSingleObject { cursor ->
64 KyberPreKey(
65 record = KyberPreKeyRecord(cursor.requireNonNullBlob(SERIALIZED)),
66 lastResort = cursor.requireBoolean(LAST_RESORT)
67 )
68 }
69 }
70
71 fun getAll(serviceId: ServiceId): List<KyberPreKey> {
72 return readableDatabase
73 .select(LAST_RESORT, SERIALIZED)
74 .from("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
75 .where("$ACCOUNT_ID = ?", serviceId.toAccountId())
76 .run()
77 .readToList { cursor ->
78 KyberPreKey(
79 record = KyberPreKeyRecord(cursor.requireNonNullBlob(SERIALIZED)),
80 lastResort = cursor.requireBoolean(LAST_RESORT)
81 )
82 }
83 }
84
85 fun getAllLastResort(serviceId: ServiceId): List<KyberPreKey> {
86 return readableDatabase
87 .select(LAST_RESORT, SERIALIZED)
88 .from("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
89 .where("$ACCOUNT_ID = ? AND $LAST_RESORT = ?", serviceId.toAccountId(), 1)
90 .run()
91 .readToList { cursor ->
92 KyberPreKey(
93 record = KyberPreKeyRecord(cursor.requireNonNullBlob(SERIALIZED)),
94 lastResort = cursor.requireBoolean(LAST_RESORT)
95 )
96 }
97 }
98
99 fun contains(serviceId: ServiceId, keyId: Int): Boolean {
100 return readableDatabase
101 .exists("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
102 .where("$ACCOUNT_ID = ? AND $KEY_ID = ?", serviceId.toAccountId(), keyId)
103 .run()
104 }
105
106 fun insert(serviceId: ServiceId, keyId: Int, record: KyberPreKeyRecord, lastResort: Boolean) {
107 writableDatabase
108 .insertInto(TABLE_NAME)
109 .values(
110 ACCOUNT_ID to serviceId.toAccountId(),
111 KEY_ID to keyId,
112 TIMESTAMP to record.timestamp,
113 SERIALIZED to record.serialize(),
114 LAST_RESORT to lastResort.toInt()
115 )
116 .run(SQLiteDatabase.CONFLICT_REPLACE)
117 }
118
119 fun deleteIfNotLastResort(serviceId: ServiceId, keyId: Int) {
120 writableDatabase
121 .delete("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
122 .where("$ACCOUNT_ID = ? AND $KEY_ID = ? AND $LAST_RESORT = ?", serviceId.toAccountId(), keyId, 0)
123 .run()
124 }
125
126 fun delete(serviceId: ServiceId, keyId: Int) {
127 writableDatabase
128 .delete("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY")
129 .where("$ACCOUNT_ID = ? AND $KEY_ID = ?", serviceId.toAccountId(), keyId)
130 .run()
131 }
132
133 fun markAllStaleIfNecessary(serviceId: ServiceId, staleTime: Long) {
134 writableDatabase
135 .update(TABLE_NAME)
136 .values(STALE_TIMESTAMP to staleTime)
137 .where("$ACCOUNT_ID = ? AND $STALE_TIMESTAMP = 0 AND $LAST_RESORT = 0", serviceId.toAccountId())
138 .run()
139 }
140
141 /**
142 * Deletes all keys that have been stale since before the specified threshold.
143 * We will always keep at least [minCount] items, preferring more recent ones.
144 */
145 fun deleteAllStaleBefore(serviceId: ServiceId, threshold: Long, minCount: Int) {
146 val count = writableDatabase
147 .delete(TABLE_NAME)
148 .where(
149 """
150 $ACCOUNT_ID = ?
151 AND $LAST_RESORT = 0
152 AND $STALE_TIMESTAMP > 0
153 AND $STALE_TIMESTAMP < $threshold
154 AND $ID NOT IN (
155 SELECT $ID
156 FROM $TABLE_NAME
157 WHERE
158 $ACCOUNT_ID = ?
159 AND $LAST_RESORT = 0
160 ORDER BY
161 CASE $STALE_TIMESTAMP WHEN 0 THEN 1 ELSE 0 END DESC,
162 $STALE_TIMESTAMP DESC,
163 $ID DESC
164 LIMIT $minCount
165 )
166 """,
167 serviceId.toAccountId(),
168 serviceId.toAccountId()
169 )
170 .run()
171
172 Log.i(TAG, "Deleted $count stale one-time EC prekeys.")
173 }
174
175 fun debugDeleteAll() {
176 writableDatabase.deleteAll(OneTimePreKeyTable.TABLE_NAME)
177 }
178
179 data class KyberPreKey(
180 val record: KyberPreKeyRecord,
181 val lastResort: Boolean
182 )
183
184 private fun ServiceId.toAccountId(): String {
185 return when (this) {
186 is ServiceId.ACI -> this.toString()
187 is ServiceId.PNI -> PNI_ACCOUNT_ID
188 }
189 }
190}