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!