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
    1. A cryptographic key is generated on the Security Key
    2. A random 32 byte secret is generated
    3. The secret is used to encrypt the database
    4. The secret is encrypted to the cryptographic key (Public-Key Cryptography) on the Security Key
    5. The encrypted secret is stored inside the app
  • The user “unlocks” the app when it is opened
    1. The encrypted secret is retrieved from the app
    2. The Security Key is used to decrypt the encrypted secret (Public-Key Cryptography)
    3. The secret is used to decrypt the database in-memory
Fork sample code on Github: Get Sample on Github

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:3.0.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:

  1. A cryptographic key is generated on the Security Key
  2. A random 32 byte secret is generated
  3. The secret is used to encrypt the database
  4. The secret is encrypted to the cryptographic key (Public-Key Cryptography) on the Security Key
  5. 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:

  1. The encrypted secret is retrieved from the app
  2. The Security Key is used to decrypt the encrypted secret (Public-Key Cryptography)
  3. 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

Profit

That’s all! If you have any questions, don’t hesitate to contact us: