FreeRDP
Loading...
Searching...
No Matches
AppDatabase.java
1/* Room database for bookmark storage */
2
3package com.freerdp.freerdpcore.data;
4
5import android.content.Context;
6
7import androidx.annotation.NonNull;
8
9import java.io.File;
10import androidx.room.Database;
11import androidx.room.Room;
12import androidx.room.RoomDatabase;
13import androidx.room.migration.Migration;
14import androidx.sqlite.db.SupportSQLiteDatabase;
15
16import com.freerdp.freerdpcore.security.KeystoreHelper;
17
18import net.zetetic.database.sqlcipher.SQLiteDatabase;
19import net.zetetic.database.sqlcipher.SupportOpenHelperFactory;
20
21@Database(entities = { BookmarkEntity.class }, version = AppDatabase.DB_VERSION,
22 exportSchema = false)
23public abstract class AppDatabase extends RoomDatabase
24{
25 static final int DB_VERSION = 12;
26 private static final String DB_NAME = "bookmarks.db";
27
28 static
29 {
30 System.loadLibrary("sqlcipher");
31 }
32
33 private static volatile AppDatabase instance;
34
35 public abstract BookmarkDao bookmarkDao();
36
37 public static AppDatabase getInstance(Context context)
38 {
39 if (instance == null)
40 {
41 synchronized (AppDatabase.class)
42 {
43 if (instance == null)
44 {
45 byte[] key = getOrCreateDbKey(context);
46 migrateUnencryptedIfNeeded(context, key);
47
48 instance = Room.databaseBuilder(context.getApplicationContext(),
49 AppDatabase.class, DB_NAME)
50 .openHelperFactory(new SupportOpenHelperFactory(key))
51 .addMigrations(MIGRATION_10_11)
52 .addMigrations(MIGRATION_11_12)
53 .build();
54 }
55 }
56 }
57 return instance;
58 }
59
60 private static byte[] getOrCreateDbKey(Context context)
61 {
62 try
63 {
64 return KeystoreHelper.getInstance(context).getOrCreateDbKey();
65 }
66 catch (KeystoreHelper.KeystoreException e)
67 {
68 throw new RuntimeException("Cannot obtain database encryption key", e);
69 }
70 }
71
72 // Converts an existing unencrypted database to SQLCipher in-place
73 private static void migrateUnencryptedIfNeeded(Context context, byte[] key)
74 {
75 File dbFile = context.getDatabasePath(DB_NAME);
76 if (!dbFile.exists())
77 return;
78
79 String path = dbFile.getAbsolutePath();
80
81 // Check if already encrypted by trying to open with the key.
82 try
83 {
84 SQLiteDatabase.openDatabase(path, key, null, SQLiteDatabase.OPEN_READONLY, null, null)
85 .close();
86 return; // Database is already encrypted, no migration needed
87 }
88 catch (Exception ignored)
89 {
90 }
91
92 SQLiteDatabase db = null;
93 try
94 {
95 // Verify it is an unencrypted database by opening it with an empty key
96 db = SQLiteDatabase.openDatabase(path, new byte[0], null, SQLiteDatabase.OPEN_READWRITE,
97 null, null);
98 }
99 catch (Exception e)
100 {
101 // If it fails, the file might be corrupted (not plaintext, not correctly encrypted)
102 SQLiteDatabase.deleteDatabase(dbFile);
103 return;
104 }
105
106 // https://www.zetetic.net/sqlcipher/sqlcipher-api/index.html#sqlcipher_export
107 // Prepare a temporary file for the encrypted database migration
108 String tmpPath = path + ".migrating";
109 File tmpFile = new File(tmpPath);
110 SQLiteDatabase.deleteDatabase(tmpFile);
111
112 try
113 {
114 // Pre-create the encrypted database to avoid CANTOPEN errors on ATTACH
115 SQLiteDatabase
116 .openDatabase(tmpPath, key, null,
117 SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY,
118 null, null)
119 .close();
120 }
121 catch (Exception ignored)
122 {
123 }
124
125 try
126 {
127 try
128 {
129 db.execSQL("ATTACH DATABASE '" + tmpPath + "' AS encrypted KEY X'" + toHex(key) +
130 "'");
131 db.rawQuery("SELECT sqlcipher_export('encrypted')", null).moveToFirst();
132 db.execSQL("DETACH DATABASE encrypted");
133 }
134 finally
135 {
136 db.close();
137 }
138
139 // Replace the old unencrypted database with the new encrypted one
140 SQLiteDatabase.deleteDatabase(dbFile);
141 if (!tmpFile.renameTo(dbFile))
142 throw new RuntimeException("Could not replace database file after encryption");
143 }
144 catch (Exception e)
145 {
146 SQLiteDatabase.deleteDatabase(tmpFile);
147 throw new RuntimeException("Failed to encrypt existing database", e);
148 }
149 }
150
151 private static String toHex(byte[] bytes)
152 {
153 StringBuilder sb = new StringBuilder(bytes.length * 2);
154 for (byte b : bytes)
155 sb.append(String.format(java.util.Locale.US, "%02x", b));
156 return sb.toString();
157 }
158
159 private static final Migration MIGRATION_11_12 = new Migration(11, 12) {
160 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
161 {
162 db.execSQL("ALTER TABLE 'bookmarks' ADD 'tlsSecLevel' CONSTRAINT chk_tlsSecLevel "
163 + "CHECK (tlsSecLevel >= -1 AND tlsSecLevel <= 5) INTEGER DEFAULT -1;");
164 db.execSQL("ALTER TABLE 'bookmarks' ADD 'tlsMinLevel' CONSTRAINT chk_tlsMinLevel "
165 + "CHECK (tlsSecLevel >= -1) INTEGER DEFAULT -1;");
166 final String list[] = { "screen_3g_colors",
167 "screen_3g_resolution",
168 "screen_3g_width",
169 "screen_3g_height",
170 "perf_3g_remotefx",
171 "perf_3g_gfx",
172 "perf_3g_gfx_h264",
173 "perf_3g_wallpaper",
174 "perf_3g_theming",
175 "perf_3g_full_window_drag",
176 "perf_3g_menu_animations",
177 "perf_3g_font_smoothing",
178 "perf_3g_desktop_composition",
179 "enable_3g_settings" };
180
181 for (String s : list)
182 {
183 db.execSQL("ALTER TABLE 'bookmarks' DROP COLUMN '" + s + "';");
184 }
185 }
186 };
187
188 // v10: tbl_manual_bookmarks + tbl_screen_settings + tbl_performance_flags (SQLiteOpenHelper)
189 // v11: single flat `bookmarks` table (Room)
190 private static final Migration MIGRATION_10_11 = new Migration(10, 11) {
191 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
192 {
193 db.execSQL("CREATE TABLE IF NOT EXISTS `bookmarks` ("
194 + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
195 + "`label` TEXT,"
196 + "`hostname` TEXT,"
197 + "`username` TEXT,"
198 + "`password` TEXT,"
199 + "`domain` TEXT,"
200 + "`port` INTEGER NOT NULL,"
201 + "`colors` INTEGER NOT NULL,"
202 + "`resolution` INTEGER NOT NULL,"
203 + "`width` INTEGER NOT NULL,"
204 + "`height` INTEGER NOT NULL,"
205 + "`perf_remotefx` INTEGER NOT NULL,"
206 + "`perf_gfx` INTEGER NOT NULL,"
207 + "`perf_gfx_h264` INTEGER NOT NULL,"
208 + "`perf_wallpaper` INTEGER NOT NULL,"
209 + "`perf_theming` INTEGER NOT NULL,"
210 + "`perf_full_window_drag` INTEGER NOT NULL,"
211 + "`perf_menu_animations` INTEGER NOT NULL,"
212 + "`perf_font_smoothing` INTEGER NOT NULL,"
213 + "`perf_desktop_composition` INTEGER NOT NULL,"
214 + "`screen_3g_colors` INTEGER NOT NULL,"
215 + "`screen_3g_resolution` INTEGER NOT NULL,"
216 + "`screen_3g_width` INTEGER NOT NULL,"
217 + "`screen_3g_height` INTEGER NOT NULL,"
218 + "`perf_3g_remotefx` INTEGER NOT NULL,"
219 + "`perf_3g_gfx` INTEGER NOT NULL,"
220 + "`perf_3g_gfx_h264` INTEGER NOT NULL,"
221 + "`perf_3g_wallpaper` INTEGER NOT NULL,"
222 + "`perf_3g_theming` INTEGER NOT NULL,"
223 + "`perf_3g_full_window_drag` INTEGER NOT NULL,"
224 + "`perf_3g_menu_animations` INTEGER NOT NULL,"
225 + "`perf_3g_font_smoothing` INTEGER NOT NULL,"
226 + "`perf_3g_desktop_composition` INTEGER NOT NULL,"
227 + "`enable_3g_settings` INTEGER NOT NULL,"
228 + "`enable_gateway_settings` INTEGER NOT NULL,"
229 + "`gateway_hostname` TEXT,"
230 + "`gateway_port` INTEGER NOT NULL,"
231 + "`gateway_username` TEXT,"
232 + "`gateway_password` TEXT,"
233 + "`gateway_domain` TEXT,"
234 + "`redirect_sdcard` INTEGER NOT NULL,"
235 + "`redirect_sound` INTEGER NOT NULL,"
236 + "`redirect_microphone` INTEGER NOT NULL,"
237 + "`security` INTEGER NOT NULL,"
238 + "`remote_program` TEXT,"
239 + "`work_dir` TEXT,"
240 + "`console_mode` INTEGER NOT NULL,"
241 + "`debug_level` TEXT,"
242 + "`async_channel` INTEGER NOT NULL,"
243 + "`async_update` INTEGER NOT NULL"
244 + ")");
245
246 // port was stored as TEXT in the old schema
247 db.execSQL(
248 "INSERT INTO bookmarks ("
249 + " id, label, hostname, username, password, domain, port,"
250 + " colors, resolution, width, height,"
251 + " perf_remotefx, perf_gfx, perf_gfx_h264, perf_wallpaper, perf_theming,"
252 + " perf_full_window_drag, perf_menu_animations, perf_font_smoothing, "
253 + "perf_desktop_composition,"
254 + " screen_3g_colors, screen_3g_resolution, screen_3g_width, screen_3g_height,"
255 + " perf_3g_remotefx, perf_3g_gfx, perf_3g_gfx_h264, perf_3g_wallpaper, "
256 + "perf_3g_theming,"
257 + " perf_3g_full_window_drag, perf_3g_menu_animations, perf_3g_font_smoothing, "
258 + "perf_3g_desktop_composition,"
259 + " enable_3g_settings, enable_gateway_settings,"
260 + " gateway_hostname, gateway_port, gateway_username, gateway_password, "
261 + "gateway_domain,"
262 + " redirect_sdcard, redirect_sound, redirect_microphone,"
263 + " security, remote_program, work_dir, console_mode,"
264 + " debug_level, async_channel, async_update"
265 + ") SELECT"
266 + " b._id, IFNULL(b.label, ''), IFNULL(b.hostname, ''), IFNULL(b.username, ''), "
267 + "b.password, b.domain,"
268 + " IFNULL(CAST(NULLIF(b.port, '') AS INTEGER), 3389),"
269 + " IFNULL(s.colors, 32), IFNULL(s.resolution, -1), IFNULL(s.width, 0), "
270 + "IFNULL(s.height, 0),"
271 + " IFNULL(p.perf_remotefx, 0), IFNULL(p.perf_gfx, 1), IFNULL(p.perf_gfx_h264, "
272 + "0), IFNULL(p.perf_wallpaper, 0), IFNULL(p.perf_theming, 0),"
273 + " IFNULL(p.perf_full_window_drag, 0), IFNULL(p.perf_menu_animations, 0), "
274 + "IFNULL(p.perf_font_smoothing, 0), IFNULL(p.perf_desktop_composition, 0),"
275 + " IFNULL(s3.colors, 16), IFNULL(s3.resolution, -1), IFNULL(s3.width, 0), "
276 + "IFNULL(s3.height, 0),"
277 + " IFNULL(p3.perf_remotefx, 0), IFNULL(p3.perf_gfx, 0), IFNULL(p3.perf_gfx_h264, "
278 + "0), IFNULL(p3.perf_wallpaper, 0), IFNULL(p3.perf_theming, 0),"
279 + " IFNULL(p3.perf_full_window_drag, 0), IFNULL(p3.perf_menu_animations, 0), "
280 + "IFNULL(p3.perf_font_smoothing, 0), IFNULL(p3.perf_desktop_composition, 0),"
281 + " IFNULL(b.enable_3g_settings, 0), IFNULL(b.enable_gateway_settings, 0),"
282 + " b.gateway_hostname, IFNULL(b.gateway_port, 443), b.gateway_username, "
283 + "b.gateway_password, b.gateway_domain,"
284 + " IFNULL(b.redirect_sdcard, 0), IFNULL(b.redirect_sound, 0), "
285 + "IFNULL(b.redirect_microphone, 0),"
286 +
287 " IFNULL(b.security, 0), b.remote_program, b.work_dir, IFNULL(b.console_mode, 0),"
288 + " IFNULL(b.debug_level, 'INFO'), IFNULL(b.async_channel, 0), "
289 + "IFNULL(b.async_update, 0)"
290 + " FROM tbl_manual_bookmarks b"
291 + " LEFT JOIN tbl_screen_settings s ON s._id = b.screen_settings"
292 + " LEFT JOIN tbl_screen_settings s3 ON s3._id = b.screen_3g"
293 + " LEFT JOIN tbl_performance_flags p ON p._id = b.performance_flags"
294 + " LEFT JOIN tbl_performance_flags p3 ON p3._id = b.performance_3g");
295
296 db.execSQL("DROP TABLE IF EXISTS tbl_manual_bookmarks");
297 db.execSQL("DROP TABLE IF EXISTS tbl_screen_settings");
298 db.execSQL("DROP TABLE IF EXISTS tbl_performance_flags");
299 }
300 };
301}