How to save, re-open, and load an SQLCipher-encrypted (Room) Database in Android?

I’m just really new to Android programming in general and by no means a professional so I hope you can help me.

I was able to encrypt and export database after reading the documentation. However, I am unsuccessful in doing the following:

  1. Re-opening the database after exporting a copy of it
  2. Loading a saved copy

This is how I built the database:

public static SampleDatabase sampleDatabase;

public static synchronized SampleDatabase getInstance(Context context) {
	if (sampleDatabase == null) {
		final byte[] passphrase = SQLiteDatabase.getBytes("userEnteredPassphrase".toCharArray());
		final SupportFactory factory = new SupportFactory(passphrase, null, false);
		sampleDatabase = Room.databaseBuilder(context.getApplicationContext(),
				SampleDatabase.class,
				"sample_database.db")
				.openHelperFactory(factory)
				.build();
	}
	return sampleDatabase;
}

In my ActivityMain, I implemented the following:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

    // import android.database.sqlite.* has been replaced by net.sqlcipher.database.*
    SQLiteDatabase.loadLibs(this);

 // other lines such as initializations of views, etc.

}

This is how I implemented an export / saving of the encrypted database:

private void copyDatabase() {
	System.out.println("is db instance open? " + SampleDatabase.getInstance(getApplicationContext()).isOpen());
	// at this point, this returns: is db instance open? true

	SampleDatabase.getInstance(getApplicationContext()).close();
	File dbfile = getDatabasePath("sample_database.db");
	File sdir = new File(getFilesDir(),"dbsavefolder");
	String sfpath = sdir.getPath() + File.separator + "sample_database.db";
	if (!sdir.exists()) {
		sdir.mkdirs();
	}
	File savefile = new File(sfpath);
	try {
		savefile.createNewFile();
		int buffersize = 8 * 1024;
		byte[] buffer = new byte[buffersize];
		int bytes_read = buffersize;
		OutputStream savedb = new FileOutputStream(sfpath);
		InputStream indb = new FileInputStream(dbfile);
		while ((bytes_read = indb.read(buffer,0,buffersize)) > 0) {
			savedb.write(buffer,0,bytes_read);
		}
		savedb.flush();
		indb.close();
		savedb.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
					
	
	System.out.println("is db instance open? " + SampleDatabase.getInstance(getApplicationContext()).isOpen());
	// now this returns: is db instance open? false
	
	SQLiteDatabase.openOrCreateDatabase(dbfile, "userEnteredPassphrase", null);
	
	System.out.println("is db instance open? " + SampleDatabase.getInstance(getApplicationContext()).isOpen());
	// this still returns: is db instance open? false
}

I thought that if I implemented

SQLiteDatabase.openOrCreateDatabase(dbfile, “userEnteredPassphrase”, null);

The expression

SampleDatabase.getInstance(getApplicationContext()).isOpen())

will return true. But it still returned false. Why is this the case? Is there a mistake in my implementation/use of SQLCipher?

Lastly, I implemented the loading of a database file by doing something like this:

private void loadDatabase() {
	SampleDatabase.getInstance(getApplicationContext()).close();
	
	try {
		// dGolez 2021-02-21-1407 todo this file is encrypted so somehow the contents must also be passed after decrytion
		copy(new File(getFilesDir(),"dbsavefolder/sample_database.db"), this.getDatabasePath("sample_database.db"));
	} catch (IOException e) {
		e.printStackTrace();
	}
	
	Intent i = getBaseContext().getPackageManager().getLaunchIntentForPackage( getBaseContext().getPackageName());
	i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
	finish();
	startActivity(i);
	System.exit(0);
}

My intention in the above method is to replace the prior database located in the main database location / getDatabasePath(), from the one saved using getFilesDir(),“dbsavefolder/sample_database.db”.

How do I proceed to do it?

Thank you! This is a great tool for someone like me who just learned Android programming a few months ago.

As far as I can tell, your problem is with your use of Room. Your use of SQLiteDatabase.openOrCreateDatabase() is unrelated to Room, yet you are asking Room if its database is open. If you want to open a Room database, use RoomDatabase.Builder. If you closed the database and wish to reopen it, you still use RoomDatabase.Builder.

I recommend that you put SQLCipher for Android aside for a bit and get your code working with ordinary Room first. Then, after you have that working, add SQLCipher for Android.

FWIW, I will have an example of import/export with SQLCipher for Android and Room in a week or two. However, those samples will be in Kotlin, not Java. It will extend this existing Room import/export sample, with SQLCipher for Android added.

Hi, thanks for a lot for answering!

I already managed to get Room working without the use of SQLCipher and was already able to do CRUD operations in it. After that, I tried using SQLCipher and replaced the imports that they said should be replaced. Am I getting it right? Or was there something else I’m supposed to do based on what you said?

Great tip. I didn’t know about this and simply just followed the documentation, so I’ll have to figure something out.

Looking forward to this. I’ll just try to take reading the example slowly and look for clues on how to make it work in Java.

Thank you.

If we ignore SQLCipher for Android, I do not see how your code would work. You close the database, then do not reopen it, then wonder why it is not open. :upside_down_face: That is not a problem with SQLCipher for Android, which is why I suggested trying to get this working with plain Room first.

My non-SQLCipher for Android example uses a repository to mediate access to the database. The repository closes the database and sets the property to null on any import or export operation. Then, it will lazy-open the database as needed on future operations.

The forthcoming SQLCipher for Android edition of the example does pretty much the same stuff. In fact, for exporting and importing the database in its encrypted form, it is identical. What the new sample adds is exporting and importing a plaintext (decrypted) form of the database, as that requires a bit of additional SQLCipher for Android work.

You close the database, then do not reopen it, then wonder why it is not open. :upside_down_face: That is not a problem with SQLCipher for Android, which is why I suggested trying to get this working with plain Room first.

Now that you mentioned it, you are actually right! I’ve just tried reading the documentation and tried using both the createFromAsset() and createFromFile() methods, but they still didn’t return true when I check if “sample_database” is open. According to the documentation:

This method is not supported for an in memory database RoomDatabase.Builder .

While at the same time, any database that was created using the build() method:

By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive triggers.

Now I am confused, because in addition to trying both the createFromAsset() and createFromFile() methods still returning false when checking if “sample_database” is open, there is also now this added information from the documentation about the in memory databases.

Did I misinterpret the documentation somehow? Where should I go from here? I want to look into your example now, but I don’t think I’ll figure anything out of it until, as you said, I got it to work using non-encrypted Room.

Thank you!

I suspect so.

You could open the database using RoomDatabase.Builder, the same way that you opened it originally. Every time you want to open the database, you use RoomDatabase.Builder. You do not need createFromAsset(), because you do not have an asset. You might use createFromFile() for the import operation, though I elected to maintain parallelism and use file copying for both import and export.

IOW, forget import, forget export, and forget SQLCipher for Android for a moment. To open a Room database, you use RoomDatabase.Builder:

Room.databaseBuilder(appContext, YourAwesomeDatabase.class, "awesome.db").build();

Once you start adding import/export/SQLCipher back in, you still use Room.databaseBuilder() to open the database, the same as you did before (with the exception of adding the SQLCipher factory). If you close() the database, to re-open it, you execute the same Room.databaseBuilder() code that you did to open it originally.

I cannot speak to that, but the code in your question is not using Room correctly.

Room.databaseBuilder(appContext, YourAwesomeDatabase.class, “awesome.db”).build();

I tried this by implementing:

 Room.databaseBuilder(getApplicationContext(),
				SampleDatabase.class,
 				"sample_database.db")
 				.build();

But the expression

SampleDatabase.getInstance(getApplicationContext()).isOpen()

is still returning false and I am still also getting this exception below:

E/ROOM: Invalidation tracker is initialized twice :/.
E/SQLiteLog: (1) no such table: room_table_modification_log
E/ROOM: Cannot run invalidation tracker. Is the db closed?
android.database.sqlite.SQLiteException: no such table: room_table_modification_log (code 1 SQLITE_ERROR): , while compiling: SELECT * FROM room_table_modification_log WHERE invalidated = 1;

I’ve tried other approaches such as trying to set the static variable for the database object to null first before calling build() again, but still no dice.

But, when I instead try to do:

Intent backupRestart = getBaseContext().getPackageManager().getLaunchIntentForPackage( getBaseContext().getPackageName());
backupRestart.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
startActivity(backupRestart);
System.exit(0);

I stopped getting exceptions (also made sure that I am still connected to “Run” console). I am now also able to execute CRUD without getting exceptions. I can’t wrap my head around this somehow. Why is it that I managed not get exceptions when I restart the activity/application, but not when I tried to simply do

 Room.databaseBuilder(getApplicationContext(),
				SampleDatabase.class,
 				"sample_database.db")
 				.build();

What happened? What did I miss?

But the expression

SampleDatabase.getInstance(getApplicationContext()).isOpen()

is still returning false

You need to replace SampleDatabase.getInstance(). Once you close that instance, get rid of it. It will be closed permanently. You need to open the database, then have SampleDatabase.getInstance() point to that newly-opened database.

Note that none of this has anything to do with SQLCipher for Android. There are other places to get support for Room, including:

  • Stack Overflow
  • The #room channel in Kotlinlang Slack workspace
  • Probably the Android United Slack workspace, though I am not a member there and so am not certain

FWIW, that code will be unreliable, with varying results across different Android OS versions and devices.

Hi, I tried to study more about destroying or nullifying static variables and found out that they will stay intact until the application’s life cycle has ended. That piece of information clued me in as to why

Intent backupRestart = getBaseContext().getPackageManager().getLaunchIntentForPackage( getBaseContext().getPackageName());
backupRestart.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
startActivity(backupRestart);
System.exit(0);

Works because it really clears up the memory for the original SampleDatabase.getInstance(getApplicationContext()).

So I tried creating a method from the Room Database class that will set the value of the static variable to null and then recreate it using build():

public static synchronized SampleDatabase destroyAndRecreateInstance(Context context) {
    
    if (sampleDatabase != null) {
        System.out.println("from SampleDatabase.java sampleDatabase = " + sampleDatabase.isOpen());
        SampleDatabase = null;
        System.out.println("sampleDatabase is nullified from SampleDatabase.java!");
    }

    sampleDatabase = Room.databaseBuilder(context.getApplicationContext(),
            SampleDatabase.class,
            "sample_database")
            .build();

    return sampleDatabase;
}

However, this is still returning false.

I understand that as of now, this is not yet about using SQLCipher, but I opted to still reply here that since this will ultimately lead to that. And future users might be able to understand more by following the process that went on here.

Thank you for helping!

FYI… I don’t know if it is a typo in the post or really in your code, but in your final code snippet, your null work is for a field named with a capital S (SampleDatabase), while the rest of destroyAndRecreateInstance() is for a field named with a lowercase s (sampleDatabase).

Thanks for noticing, it’s just a typo on my part. Fixed it now. Thanks!

@commonsguy so far the only solution I got working is still this

Intent backupRestart = getBaseContext().getPackageManager().getLaunchIntentForPackage( getBaseContext().getPackageName());
backupRestart.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
startActivity(backupRestart);
System.exit(0);

While my other attempts at destroying SampleDatabase.getInstance(); never really works.

Can you give me an explanation as to why you said this?

FWIW, that code will be unreliable, with varying results across different Android OS versions and devices.

Also, I’m at the end now as to why I couldn’t destroy my first instance of SampleDatabase. I’ve tried multiple approaches now including overriding onDestroy() and checking the address of getInstance(context) and seeing that it is still not null even if I tried doing sampleDatabase = null.

This is where I’m stuck now: I just can’t get the first instance to null and reopen the database with SQLCipher factory helper except where I had to restart the activity, which to be honest I am not really fully sure how it worked.

First, using System.exit() has been considered bad form in Android from the beginning. You are assuming that everything gets cleaned up properly. It should get cleaned up properly, but there are lots of things in programming where “should” does not happen.

Second, you have set up a race condition between startActivity() having some sort of effect and your process being terminated via System.exit(). What happens if your process is terminated first?

Third, since background activity starts have been banned for some time, you are assuming that a freshly-killed process somehow counts as being foreground enough that your startActivity() call is allowed.

And all of this can vary by device manufacturer and device, as these behaviors that you are relying upon (e.g., the behavior of System.exit(), the timing of startActivity()) are undocumented. So, just because you happen to get reasonable results on a couple of test devices, do not assume that you will get reasonable results across 26,000+ Android device models, plus past/present/future versions of Android.

(BTW, you do not need getBaseContext() here)

(and none of this has anything to do with SQLCipher for Android)

Ok this explains a lot. And I think I’m now arriving the right question that should be asked. Hopefully I can make this work “correctly” soon.

Thanks!

Hi,

I really hope you don’t mind getting this question despite my problem seemingly not involved with SQLCipher directly as it appears.

Thanks to your tips! I’m figuring out how to research things and getting more options on what to try and I finally, somehow, was able to set the first static instance of SampleDatabase to null and recreate another one. I was able to do it by:

  1. Implementing a method that sets the instance to null

public static synchronized SampleDatabase destroyAndRecreateInstance(Context context) {

if (sampleDatabase != null) {
    System.out.println("from SampleDatabase.java sampleDatabase = " + sampleDatabase.isOpen());
    SampleDatabase = null;
    System.out.println("sampleDatabase is nullified from SampleDatabase.java!");
}

sampleDatabase = Room.databaseBuilder(context.getApplicationContext(),
        SampleDatabase.class,
        "sample_database")
        .build();

return sampleDatabase;

}

  1. Calling destroyAndRecreateInstance() in the onDestroy() method of the activity:

@Override
protected void onDestroy() {
SampleDatabase.destroyAndRecreateInstance(getApplicationContext());
super.onDestroy();
}

  1. Calling this.recreate() on the method that implements the backup procedure.

Step 1 returns the database static instance as null, and returns a non-null value after calling build(), however, I am encountering this exception once I try to engage the database in any CRUD operation:

E/ROOM: Invalidation tracker is initialized twice :/.
E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_1
Process: net.mysample.sampledatabase, PID: 7150
net.sqlcipher.database.SQLiteException: no such table: room_table_modification_log: , while compiling: SELECT * FROM room_table_modification_log WHERE invalidated = 1;
at net.sqlcipher.database.SQLiteCompiledSql.native_compile(Native Method)
at net.sqlcipher.database.SQLiteCompiledSql.compile(SQLiteCompiledSql.java:89)
at net.sqlcipher.database.SQLiteCompiledSql.(SQLiteCompiledSql.java:62)
at net.sqlcipher.database.SQLiteProgram.(SQLiteProgram.java:91)
at net.sqlcipher.database.SQLiteQuery.(SQLiteQuery.java:55)
at net.sqlcipher.database.SQLiteDatabase.query(SQLiteDatabase.java:2958)
at net.sqlcipher.database.SQLiteDatabase.query(SQLiteDatabase.java:2948)
at androidx.room.RoomDatabase.query(RoomDatabase.java:328)
at androidx.room.RoomDatabase.query(RoomDatabase.java:311)
at androidx.room.InvalidationTracker$1.checkUpdatedTable(InvalidationTracker.java:414)
at androidx.room.InvalidationTracker$1.run(InvalidationTracker.java:388)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)

I found that this is could be related to some issue in Room itself. Is this somehow correct? What are your thoughts on this? I found myself digging quite deeper now than my current level of understanding so any tips will do. Thank you!