That fuck shit the fascists are using
1package org.tm.archive.database;
2
3import android.app.Application;
4import android.content.ContentValues;
5import android.content.Context;
6import android.database.Cursor;
7
8import androidx.annotation.NonNull;
9
10import net.zetetic.database.sqlcipher.SQLiteDatabase;
11import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
12
13import org.signal.core.util.concurrent.SignalExecutors;
14import org.signal.core.util.logging.Log;
15import org.tm.archive.crypto.DatabaseSecret;
16import org.tm.archive.crypto.DatabaseSecretProvider;
17import org.tm.archive.keyvalue.KeyValueDataSet;
18import org.tm.archive.keyvalue.KeyValuePersistentStorage;
19import org.signal.core.util.CursorUtil;
20
21import java.util.Collection;
22import java.util.Map;
23
24/**
25 * Persists data for the {@link org.tm.archive.keyvalue.KeyValueStore}.
26 *
27 * This is it's own separate physical database, so it cannot do joins or queries with any other
28 * tables.
29 */
30public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabaseOpenHelper, KeyValuePersistentStorage {
31
32 private static final String TAG = Log.tag(KeyValueDatabase.class);
33
34 private static final int DATABASE_VERSION = 1;
35 private static final String DATABASE_NAME = "signal-key-value.db";
36
37 private static final String TABLE_NAME = "key_value";
38 private static final String ID = "_id";
39 private static final String KEY = "key";
40 private static final String VALUE = "value";
41 private static final String TYPE = "type";
42
43 private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
44 KEY + " TEXT UNIQUE, " +
45 VALUE + " TEXT, " +
46 TYPE + " INTEGER)";
47
48 private static volatile KeyValueDatabase instance;
49
50 private final Application application;
51
52 public static @NonNull KeyValueDatabase getInstance(@NonNull Application context) {
53 if (instance == null) {
54 synchronized (KeyValueDatabase.class) {
55 if (instance == null) {
56 SqlCipherLibraryLoader.load();
57 instance = new KeyValueDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
58 }
59 }
60 }
61 return instance;
62 }
63
64 public static boolean exists(Context context) {
65 return context.getDatabasePath(DATABASE_NAME).exists();
66 }
67
68
69 private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
70 super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook(), true);
71
72 this.application = application;
73 }
74
75 @Override
76 public void onCreate(SQLiteDatabase db) {
77 Log.i(TAG, "onCreate()");
78
79 db.execSQL(CREATE_TABLE);
80
81 if (SignalDatabase.hasTable("key_value")) {
82 Log.i(TAG, "Found old key_value table. Migrating data.");
83 migrateDataFromPreviousDatabase(SignalDatabase.getRawDatabase(), db);
84 }
85 }
86
87 @Override
88 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
89 Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
90 }
91
92 @Override
93 public void onOpen(SQLiteDatabase db) {
94 Log.i(TAG, "onOpen()");
95
96 db.setForeignKeyConstraintsEnabled(true);
97
98 SignalExecutors.BOUNDED.execute(() -> {
99 if (SignalDatabase.hasTable("key_value")) {
100 Log.i(TAG, "Dropping original key_value table from the main database.");
101 SignalDatabase.getRawDatabase().execSQL("DROP TABLE key_value");
102 }
103 });
104 }
105
106 @Override
107 public @NonNull KeyValueDataSet getDataSet() {
108 KeyValueDataSet dataSet = new KeyValueDataSet();
109
110 try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){
111 while (cursor != null && cursor.moveToNext()) {
112 Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)));
113 String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY));
114
115 switch (type) {
116 case BLOB:
117 dataSet.putBlob(key, cursor.getBlob(cursor.getColumnIndexOrThrow(VALUE)));
118 break;
119 case BOOLEAN:
120 dataSet.putBoolean(key, cursor.getInt(cursor.getColumnIndexOrThrow(VALUE)) == 1);
121 break;
122 case FLOAT:
123 dataSet.putFloat(key, cursor.getFloat(cursor.getColumnIndexOrThrow(VALUE)));
124 break;
125 case INTEGER:
126 dataSet.putInteger(key, cursor.getInt(cursor.getColumnIndexOrThrow(VALUE)));
127 break;
128 case LONG:
129 dataSet.putLong(key, cursor.getLong(cursor.getColumnIndexOrThrow(VALUE)));
130 break;
131 case STRING:
132 dataSet.putString(key, cursor.getString(cursor.getColumnIndexOrThrow(VALUE)));
133 break;
134 }
135 }
136 }
137
138 return dataSet;
139 }
140
141 @Override
142 public void writeDataSet(@NonNull KeyValueDataSet dataSet, @NonNull Collection<String> removes) {
143 SQLiteDatabase db = getWritableDatabase();
144
145 db.beginTransaction();
146 try {
147 for (Map.Entry<String, Object> entry : dataSet.getValues().entrySet()) {
148 String key = entry.getKey();
149 Object value = entry.getValue();
150 Class type = dataSet.getType(key);
151
152 ContentValues contentValues = new ContentValues(3);
153 contentValues.put(KEY, key);
154
155 if (type == byte[].class) {
156 contentValues.put(VALUE, (byte[]) value);
157 contentValues.put(TYPE, Type.BLOB.getId());
158 } else if (type == Boolean.class) {
159 contentValues.put(VALUE, (boolean) value);
160 contentValues.put(TYPE, Type.BOOLEAN.getId());
161 } else if (type == Float.class) {
162 contentValues.put(VALUE, (float) value);
163 contentValues.put(TYPE, Type.FLOAT.getId());
164 } else if (type == Integer.class) {
165 contentValues.put(VALUE, (int) value);
166 contentValues.put(TYPE, Type.INTEGER.getId());
167 } else if (type == Long.class) {
168 contentValues.put(VALUE, (long) value);
169 contentValues.put(TYPE, Type.LONG.getId());
170 } else if (type == String.class) {
171 contentValues.put(VALUE, (String) value);
172 contentValues.put(TYPE, Type.STRING.getId());
173 } else {
174 throw new AssertionError("Unknown type: " + type);
175 }
176
177 db.insertWithOnConflict(TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);
178 }
179
180 String deleteQuery = KEY + " = ?";
181 for (String remove : removes) {
182 db.delete(TABLE_NAME, deleteQuery, new String[] { remove });
183 }
184
185 db.setTransactionSuccessful();
186 } finally {
187 db.endTransaction();
188 }
189 }
190
191 @Override
192 public @NonNull SQLiteDatabase getSqlCipherDatabase() {
193 return getWritableDatabase();
194 }
195
196 private static void migrateDataFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
197 try (Cursor cursor = oldDb.rawQuery("SELECT * FROM key_value", null)) {
198 while (cursor.moveToNext()) {
199 int type = CursorUtil.requireInt(cursor, "type");
200 ContentValues values = new ContentValues();
201 values.put(KEY, CursorUtil.requireString(cursor, "key"));
202 values.put(TYPE, type);
203
204 switch (type) {
205 case 0:
206 values.put(VALUE, CursorUtil.requireBlob(cursor, "value"));
207 break;
208 case 1:
209 values.put(VALUE, CursorUtil.requireBoolean(cursor, "value"));
210 break;
211 case 2:
212 values.put(VALUE, CursorUtil.requireFloat(cursor, "value"));
213 break;
214 case 3:
215 values.put(VALUE, CursorUtil.requireInt(cursor, "value"));
216 break;
217 case 4:
218 values.put(VALUE, CursorUtil.requireLong(cursor, "value"));
219 break;
220 case 5:
221 values.put(VALUE, CursorUtil.requireString(cursor, "value"));
222 break;
223 }
224
225 newDb.insert(TABLE_NAME, null, values);
226 }
227 }
228 }
229
230 private enum Type {
231 BLOB(0), BOOLEAN(1), FLOAT(2), INTEGER(3), LONG(4), STRING(5);
232
233 final int id;
234
235 Type(int id) {
236 this.id = id;
237 }
238
239 public int getId() {
240 return id;
241 }
242
243 public static Type fromId(int id) {
244 return values()[id];
245 }
246 }
247}