Encrypted Database
In this guide, you’ll learn how to implement an encrypted Android database that is only decrypted with a specific Security Key. It uses our Hardware Security SDK and the Room Persistence Library. Internally, SQLCipher and CWAC-SafeRoom are used.
The main workflows are:
- The user pairs a Security Key with the app during the on-boarding process
- A cryptographic key is generated on the Security Key
- A random 32 byte secret is generated
- The secret is used to encrypt the database
- The secret is encrypted to the cryptographic key (Public-Key Cryptography) on the Security Key
- The encrypted secret is stored inside the app
- The user “unlocks” the app when it is opened
- The encrypted secret is retrieved from the app
- The Security Key is used to decrypt the encrypted secret (Public-Key Cryptography)
- The secret is used to decrypt the database in-memory
Add the SDK to Your Project
To get a username and password for our Maven repository, please contact us for a license.
Add this to your build.gradle
:
repositories {
google()
jcenter()
maven {
credentials {
username 'xxx'
password 'xxx'
}
url "https://maven.cotech.de"
}
// CWAC-SafeRoom Maven repository
maven { url "https://s3.amazonaws.com/repo.commonsware.com" }
}
dependencies {
// OpenPGP Card Specification
implementation 'de.cotech:hwsecurity-openpgp:4.4.0'
// Room Persistence Library
implementation "androidx.room:room-runtime:2.1.0"
annotationProcessor "androidx.room:room-compiler:2.1.0"
// SQLCipher and CWAC-SafeRoom
implementation "com.commonsware.cwac:saferoom.x:1.1.2"
implementation "net.zetetic:android-database-sqlcipher:4.2.0@aar"
}
Initialize the Hardware Security SDK
To use the SDK’s functionality in your app, you need to initialize the SecurityKeyManager
first.
This is the central class of the SDK, which dispatches incoming NFC and USB connections.
Perform this initialization in the onCreate
method of your Application
subclass.
This ensures Security Keys are reliably dispatched by your app while in the foreground.
We start by creating a new class which extends android.app.Application
as follows:
public class MyCustomApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SecurityKeyManager securityKeyManager = SecurityKeyManager.getInstance();
SecurityKeyManagerConfig config = new SecurityKeyManagerConfig.Builder()
.setEnableDebugLogging(BuildConfig.DEBUG)
.build();
securityKeyManager.init(this, config);
}
}
class MyCustomApplication : Application() {
override fun onCreate() {
super.onCreate()
val securityKeyManager = SecurityKeyManager.getInstance()
val config = SecurityKeyManagerConfig.Builder()
.setEnableDebugLogging(BuildConfig.DEBUG)
.build()
securityKeyManager.init(this, config)
}
}
Then, register your MyCustomApplication
in your AndroidManifest.xml
:
<application
android:name=".MyCustomApplication"
android:label="@string/app_name"
...>
Room
Following the basic Training Guide for Room, we create one simple entity and one DAO:
@Entity
public class User {
@PrimaryKey
public int uid;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@NonNull
@Override
public String toString() {
return "uid=" + uid + "\nfirst_name=" + firstName + "\nlast_name=" + lastName;
}
}
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
Database Instance
The RoomDatabase
allows decryption with a ByteSecret
.
It is implemented as a Singelton to ensure that only one database is decrypted and held in-memory.
@Database(entities = {User.class}, version = 1)
public abstract class EncryptedDatabase extends RoomDatabase {
private static EncryptedDatabase sInstance;
@VisibleForTesting
public static final String DATABASE_NAME = "encrypted-sample-db";
public static EncryptedDatabase decryptAndGetInstance(final Context context, ByteSecret secret) {
if (sInstance == null) {
synchronized (EncryptedDatabase.class) {
if (sInstance == null) {
sInstance = buildDatabase(context.getApplicationContext(), secret);
}
}
}
return sInstance;
}
public static EncryptedDatabase getInstance() {
if (sInstance == null) {
return null;
} else {
return sInstance;
}
}
private static EncryptedDatabase buildDatabase(final Context appContext, ByteSecret secret) {
SafeHelperFactory factory = new SafeHelperFactory(secret.getByteCopyAndClear());
return Room.databaseBuilder(appContext, EncryptedDatabase.class, DATABASE_NAME)
.openHelperFactory(factory)
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
// TODO: populate database with initial data
}
})
.build();
}
public abstract UserDao userDao();
}
Base Activity
The BaseActivity
ensures that the user first pairs a Security Key with the app and decrypts the database before usage.
All normal Activities in your app should extend this BaseActivity
for this (see
MainActivity
).
class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidPreferencesEncryptedSessionStorage masterSecretPrefs = AndroidPreferencesEncryptedSessionStorage.getInstance(this);
boolean hasNoSecret = !masterSecretPrefs.hasAnyEncryptedSessionSecret();
if (hasNoSecret) {
startSetup();
return;
}
if (EncryptedDatabase.getInstance() == null) {
decryptDatabase();
return;
}
}
void startSetup() {
Intent intent = new Intent(this, SetupActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
finish();
}
void decryptDatabase() {
Intent intent = new Intent(this, DecryptActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
finish();
}
}
SetupActivity
The SetupActivity
is started when no Security Key has been paired with the app, i.e. the database has not been initialized before.
It executes the following steps:
- A cryptographic key is generated on the Security Key
- A random 32 byte secret is generated
- The secret is used to encrypt the database
- The secret is encrypted to the cryptographic key (Public-Key Cryptography) on the Security Key
- The encrypted secret is stored inside the app
public class SetupActivity extends AppCompatActivity implements SecurityKeyCallback<OpenPgpSecurityKey> {
private PinProvider pinProvider;
private PairedSecurityKeyStorage pairedSecurityKeyStorage;
private boolean showWipeDialog = true;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setup);
SecurityKeyManager.getInstance().registerCallback(
OpenPgpSecurityKeyConnectionMode.getInstance(), this, this);
pinProvider =
AndroidPreferenceSimplePinProvider.getInstance(getApplicationContext());
pairedSecurityKeyStorage =
AndroidPreferencePairedSecurityKeyStorage.getInstance(getApplicationContext());
}
@Override
public void onSecurityKeyDiscovered(@NonNull OpenPgpSecurityKey securityKey) {
if (showWipeDialog && !securityKey.isSecurityKeyEmpty()) {
DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
showWipeDialog = false;
break;
case DialogInterface.BUTTON_NEGATIVE:
break;
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("The Security Key is NOT empty! Wipe and generate a new key?")
.setPositiveButton("Yes", dialogClickListener)
.setNegativeButton("No", dialogClickListener)
.show();
} else {
setupDatabase(securityKey);
}
}
@Override
public void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) {
}
@Override
public void onSecurityKeyDisconnected(@NonNull OpenPgpSecurityKey securityKey) {
}
private void setupDatabase(OpenPgpSecurityKey securityKey) {
// TODO: use something better than AsyncTask in your real app!
@SuppressLint("StaticFieldLeak")
AsyncTask task = new AsyncTask<Object, Object, String>() {
@Override
protected String doInBackground(Object[] objects) {
PairedSecurityKey pairedSecurityKey = pairAndStoreSecurityKey(securityKey);
if (pairedSecurityKey == null) {
return "failed to generate keys and pair Security Key!";
}
ByteSecret secret = generateSecret();
byte[] encryptedSecret = encryptToSecurityKey(pairedSecurityKey, secret);
saveEncryptedSecret(pairedSecurityKey, encryptedSecret);
EncryptedDatabase.decryptAndGetInstance(getApplicationContext(), secret);
return "successfully paired key, encrypted database with random secret that is encrypted to the security key";
}
@Override
protected void onPostExecute(String returnString) {
super.onPostExecute(returnString);
Toast.makeText(SetupActivity.this, returnString, Toast.LENGTH_LONG).show();
Intent intent = new Intent(SetupActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
};
task.execute();
}
private PairedSecurityKey pairAndStoreSecurityKey(OpenPgpSecurityKey securityKey) {
try {
// OpenPgpSecurityKey operations are blocking, consider executing them in a new thread
PairedSecurityKey pairedSecurityKey = securityKey.setupPairedKey(pinProvider);
// Store the pairedSecurityKey. That way we can use it for encryption at any point
pairedSecurityKeyStorage.addPairedSecurityKey(pairedSecurityKey);
return pairedSecurityKey;
} catch (IOException e) {
return null;
}
}
public ByteSecret generateSecret() {
SecretGenerator secretGenerator = SecretGenerator.getInstance();
return secretGenerator.createRandom(32);
}
public byte[] encryptToSecurityKey(PairedSecurityKey pairedSecurityKey, ByteSecret secret) {
return new PairedEncryptor(pairedSecurityKey).encrypt(secret);
}
private void saveEncryptedSecret(PairedSecurityKey pairedSecurityKey, byte[] encryptedSecret) {
EncryptedSessionStorage encryptedSessionStorage =
AndroidPreferencesEncryptedSessionStorage.getInstance(getApplicationContext());
encryptedSessionStorage.setEncryptedSessionSecret(
pairedSecurityKey.getSecurityKeyAid(), encryptedSecret);
}
}
DecryptActivity
The DecryptActivity
is started when no decrypted database instance is currently held in-memory.
It executes the following steps:
- The encrypted secret is retrieved from the app
- The Security Key is used to decrypt the encrypted secret (Public-Key Cryptography)
- The secret is used to decrypt the database in-memory
public class DecryptActivity extends AppCompatActivity implements SecurityKeyCallback<OpenPgpSecurityKey> {
private PinProvider pinProvider;
private PairedSecurityKeyStorage pairedSecurityKeyStorage;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_unlock);
SecurityKeyManager.getInstance().registerCallback(
OpenPgpSecurityKeyConnectionMode.getInstance(), this, this);
pinProvider =
AndroidPreferenceSimplePinProvider.getInstance(getApplicationContext());
pairedSecurityKeyStorage =
AndroidPreferencePairedSecurityKeyStorage.getInstance(getApplicationContext());
}
@Override
public void onSecurityKeyDiscovered(@NonNull OpenPgpSecurityKey securityKey) {
decryptDatabase(securityKey);
}
@Override
public void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) {
}
@Override
public void onSecurityKeyDisconnected(@NonNull OpenPgpSecurityKey securityKey) {
}
private void decryptDatabase(OpenPgpSecurityKey securityKey) {
// TODO: use something better than AsyncTask in your real app!
@SuppressLint("StaticFieldLeak")
AsyncTask task = new AsyncTask<Object, Object, String>() {
@Override
protected String doInBackground(Object[] objects) {
PairedSecurityKey pairedSecurityKey = getPairedSecurityKey();
if (pairedSecurityKey == null) {
return "failed to get paired security key";
}
byte[] encryptedSecret = getEncryptedSecret(pairedSecurityKey);
ByteSecret secret = decrypt(securityKey, encryptedSecret);
if (secret == null) {
return "decrypt failed";
}
// decrypt database
EncryptedDatabase.decryptAndGetInstance(getApplicationContext(), secret);
return "successfully decrypted database!";
}
@Override
protected void onPostExecute(String returnString) {
super.onPostExecute(returnString);
Toast.makeText(DecryptActivity.this, returnString, Toast.LENGTH_LONG).show();
Intent intent = new Intent(DecryptActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
};
task.execute();
}
private PairedSecurityKey getPairedSecurityKey() {
// for simplicity, we assume a single paired security key
return pairedSecurityKeyStorage.getAllPairedSecurityKeys().iterator().next();
}
private byte[] getEncryptedSecret(PairedSecurityKey pairedSecurityKey) {
EncryptedSessionStorage encryptedSessionStorage =
AndroidPreferencesEncryptedSessionStorage.getInstance(getApplicationContext());
return encryptedSessionStorage.getEncryptedSessionSecret(pairedSecurityKey.getSecurityKeyAid());
}
public ByteSecret decrypt(OpenPgpSecurityKey securityKey, byte[] encryptedSecret) {
try {
PairedSecurityKey pairedSecurityKey = pairedSecurityKeyStorage.getPairedSecurityKey(
securityKey.getOpenPgpInstanceAid());
OpenPgpPairedDecryptor decryptor =
new OpenPgpPairedDecryptor(securityKey, pinProvider, pairedSecurityKey);
return decryptor.decryptSessionSecret(encryptedSecret);
} catch (IOException e) {
return null;
}
}
}
Database Operations with Room
All your Activities should extend the BaseActivity
to ensure previous Security Key setup and database decryption.
Database operations can be done as specified by the
Room Persistence Library.
public class MainActivity extends BaseActivity {
private void insert() {
// TODO: use your favorite way of threading in your app
new Thread(() -> {
User testUser = new User();
testUser.firstName = "Martin";
testUser.lastName = "Sonneborn";
try {
EncryptedDatabase.getInstance().userDao().insertAll(testUser);
} catch (SQLiteConstraintException e) {
MainActivity.this.runOnUiThread(() -> Toast.makeText(MainActivity.this, "user already inserted", Toast.LENGTH_LONG).show());
return;
}
MainActivity.this.runOnUiThread(() -> Toast.makeText(MainActivity.this, "users successfully inserted", Toast.LENGTH_LONG).show());
}).start();
}
private void query() {
// TODO: use your favorite way of threading in your app
new Thread(() -> {
List<User> users = EncryptedDatabase.getInstance().userDao().getAll();
MainActivity.this.runOnUiThread(() -> Toast.makeText(MainActivity.this, "users: " + users.toString(), Toast.LENGTH_LONG).show());
}).start();
}
}
Prevent Re-Creation of Activity with USB Security Keys
Besides the functionalities used by our SDK, some Security Keys register themselves as USB keyboards to be able to insert One Time Passwords (OTP) when touching the golden disc. Thus, when inserting a Security Key into the USB port, Android recognizes a new keyboard and re-creates the current activity.
To prevent this, add keyboard|keyboardHidden
to the activity’s configChanges
in your AndroidManifest.xml
:
<activity
android:name=".MyCustomActivity"
android:configChanges="keyboard|keyboardHidden"
... >
Conclusion
This guide shows a general way of implementing an encrypted database using Security Keys over NFC and USB. Some details are left to the developer, which are not covered here, such as:
- Better threading outside the main UI thread, for example with Kotlin’s coroutines
- Pairing of multiple Security Keys
- Preferred user interface
- Error handling
Congratulations!
That’s all! If you have any questions, don’t hesitate to contact us: