Skip to content

Secure Store

The Secure Store module provides transparent 256-bit AES encryption of data stored in a SQLite database. This module supports three storage types:

Database

SecureDatabaseStore represents a single SQLite relational database that can be used by executing SQL statements.

Creating the Database

Instantiate the database by:

  • Providing your Android application context, database name, and database version, and
  • Implementing CreateDatabaseCallback callback methods, where you create or update database tables and other database objects.
private static final String DATABASE_NAME = "myDatabase";
private static final int DATABASE_VERSION = 1; // Increment the version number when there are schema changes
private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS users (name TEXT, age INTEGER, email TEXT NOT NULL UNIQUE PRIMARY KEY)";

SecureDatabaseStore store = new SecureDatabaseStore(context,
    DATABASE_NAME,
    DATABASE_VERSION,
    new CreateDatabaseCallback() {
        @Override
        public void onCreate(SecureDatabaseStore store) {
            store.executeUpdate(CREATE_TABLE);
        }

        @Override
        public void onUpgrade(SecureDatabaseStore store, int oldVersion, int newVersion) {
        // This method is invoked when the database version passed to the
        // constructor is different from the version of the database on device.

        // Modify the existing table or migrate the original table data based on the
        // differences across each version.
        if (oldVersion < 2) {
            ...
        }

        if (oldVersion < 3) {
            ...
        }

        // Now the app will iterate over the update statements and run any that are needed.
        // No matter what previous version was and regardless of what more recent version
        // they upgrade to, the app will run the proper statements to take the app from the
        // older schema to the properly upgraded one.
        }
    });
companion object {
    private val logger = LoggerFactory.getLogger(SecureDatabaseStoreTest::class.java)

    private val DATABASE_NAME = "myDatabase"
    private val DATABASE_VERSION = 1 // Increment the version number when there are schema changes
    private val CREATE_TABLE = "CREATE TABLE IF NOT EXISTS users (name TEXT, age INTEGER, email TEXT NOT NULL UNIQUE PRIMARY KEY)"
}

val store = SecureDatabaseStore(context!!, DATABASE_NAME, DATABASE_VERSION,
    object : CreateDatabaseCallback {
            override fun onCreate(store: SecureDatabaseStore) {
                store.executeUpdate(CREATE_TABLE)
            }

            override fun onUpgrade(store: SecureDatabaseStore, oldVersion: Int, newVersion: Int) {
                // This method is invoked when the database version passed
                // to the constructor is different from the version of the database on device.

                // Modify the existing table or migrate the original table data based on the
                // differences across each version.
                if (oldVersion < 2) {
                    ...
                }

                if (oldVersion < 3) {
                    ...
                }

                // Now the app will iterate over the update statements and run any that are needed.
                // No matter what previous version was and regardless of what more recent version
                // they upgrade to, the app will run the proper statements to take the app from the
                // older schema to the properly upgraded one.
            }
    })

Opening the Database

You must open the store with an encryption key before you can interact with it. The encryption key is used to encrypt the underlying persistence store.

SAP recommends that you use the encryption utility to obtain the encryption key. If it is the first time the store is opened and null is provided as the encryption key, a key is generated transparently and is used for subsequent open calls. See Encryption Utility for more information.

The store fails to open if there are insufficient resources or permissions to open and/or create the store. The store can also fail to open if the encryption key is incorrect.

try {
final byte[] myEncryptionKey = EncryptionUtil.getEncryptionKey("aliasForMyDatabase", myPasscode);
    store.open(myEncryptionKey);

// Or passes null and the store will generate an encryption key which can be
// used for subsequent open calls.
// For example,
//   store.open(null);
} catch (OpenFailureException ex) {
    logger.error("An error occurred while opening the database.", ex);
} catch (EncryptionError ex) {
logger.error("Failed to get encryption key.", ex);
}
try {
    val myEncryptionKey = EncryptionUtil.getEncryptionKey("aliasForMyDatabase", myPasscode)
    store.open(myEncryptionKey)

    // Or passes null and the store will generate an encryption key which can be used for subsequent open calls.
    // For example,
    //   store.open(null);
} catch (ex: OpenFailureException) {
    logger.error("An error occurred while opening the database.", ex)
} catch (ex: EncryptionError) {
    logger.error("Failed to get encryption key", ex)
}

Updating the Database

Any SQL statement that is not a SELECT statement qualifies as an update, including statements such as CREATE, UPDATE, INSERT, COMMIT, DELETE, REPLACE, and so on.

Executing updates can throw errors if something fails:

// Inserts entries.
final String insertSQL = "INSERT INTO users (email, name, age) VALUES(?,?,?)";

try {
    store.executeUpdate(insertSQL,     // INSERT statement with parameters '?'.
        "john.smith@company.com",      // Email.
        "John Smith",                  // Name.
        39);                           // Age.

    store.executeUpdate(insertSQL, "mary.clark@company.com", "Mary Clark", 21);
} catch (BackingStoreException ex) {
    logger.error("Failed to insert.", ex);
}
val insertSQL = "INSERT INTO users (email, name, age) VALUES(?,?,?)"

try {
    store.executeUpdate(insertSQL, // INSERT statement with parameters '?'.
        "john.smith@company.com",  // Email.
        "John Smith",              // Name.
        39)                        // Age.

    store.executeUpdate(insertSQL, "mary.clark@company.com", "Mary Clark", 21)
} catch (ex: BackingStoreException) {
    logger.error("Failed to insert.", ex)
}

Executing Queries on the Database

A SELECT statement is a query and executes using one of the executeQuery(...) methods. Executing a query returns a SecureDatabaseResultSet object if successful and throws an error on failure.

You typically use a while() loop to iterate over the results of the query. You also need to step from one record to the next.

// Queries the database and iterates through the rows in the result set.
final String queryAllUsers = "SELECT * FROM users";
try (SecureDatabaseResultSet rs = store.executeQuery(queryAllUsers)) {
    while (rs.next()) {
        // Has a valid row, retrieves the column values with appropriate getter method.
        int age = rs.getInt("age");
        String name = rs.getString("name");
        String email = rs.getString("email");
        logger.debug("Retrieved user: age = {}, name = {}, email = {}", age, name, email);
}
} catch (BackingStoreException ex) {
    logger.error("Failed to execute query.", ex);
}
val queryAllUsers = "SELECT * FROM users"
try {
    store.executeQuery(queryAllUsers).use { rs ->
        while (rs.next()) {
            // Has a valid row, retrieves the column values with appropriate getter method.
            val age = rs.getInt("age")!!
            val name = rs.getString("name")
            val email = rs.getString("email")
            logger.debug("Retrieved user: age = {}, name = {}, email = {}", age, name, email)
        }
    }
} catch (ex: BackingStoreException) {
    logger.error("Failed to execute query.", ex)
}

You must always invoke SecureDatabaseResultSet.next() before attempting to access the values returned in a query, even if you are only expecting one row in the result set.

int userCount = -1;
try (SecureDatabaseResultSet rs =  store.executeQuery("SELECT COUNT(*) AS count FROM users")) {
    if (rs.next()) {
        userCount = rs.getInt(0);
        logger.debug("Number of users: {}", userCount);
    }
} catch (BackingStoreException ex) {
    logger.error("Failed to get user count.", ex);
}
var userCount: Int = -1
try {
    store.executeQuery("SELECT COUNT(*) AS count FROM users").use { rs ->
        if (rs.next()) {
            userCount = rs.getInt(0)!!
            logger.debug("Number of users: {}", userCount)
        }
    }
} catch (ex: BackingStoreException) {
logger.error("Failed to get user count.", ex)
}

A SecureDatabaseResultSet contains many methods for retrieving different data types in an appropriate format.

Each data retrieval method has two variants to retrieve the data based on the position of the column in the results, as opposed to the column’s name:

  • getInt
  • getInt16
  • getInt64
  • getDouble
  • getFloat
  • getBoolean
  • getBlob
  • getString

SecureDatabaseResultSet needs to be closed. It will be closed automatically if you use a try-with block. Otherwise, you need to call the close() method explicitly.

Closing the Database

When you finish executing queries and updates on the database, close the SecureDatabaseStore to relinquishes any resources it has acquired during its operations:

store.close();
store.close()

Multiple Statements and Batch Commands

You can use the SecureDatabaseStore executeStatements(String...sqlStatements) to execute multiple SQL statements. These statements are executed in a transaction internally: If one or more of the SQL statements fail, the transaction is rolled back as if no statements were executed.

try {
    store.executeStatements(
        "CREATE TABLE test1 (id INTEGER PRIMARY KEY AUTOINCREMENT, x TEXT)",
        "CREATE TABLE test2 (id INTEGER PRIMARY KEY AUTOINCREMENT, y TEXT)",
        "CREATE TABLE test3 (id INTEGER PRIMARY KEY AUTOINCREMENT, z TEXT)",
        "INSERT INTO test1 (x) VALUES ('XXX')",
        "INSERT INTO test2 (y) VALUES ('YYY')",
        "INSERT INTO test3 (z) VALUES ('ZZZ')"
    );
} catch (BackingStoreException ex) {
    logger.error("An error occurred while executing SQL statements on the database.", ex);
}
try {
    store.executeStatements(
        "CREATE TABLE test1 (id INTEGER PRIMARY KEY AUTOINCREMENT, x TEXT)",
        "CREATE TABLE test2 (id INTEGER PRIMARY KEY AUTOINCREMENT, y TEXT)",
        "CREATE TABLE test3 (id INTEGER PRIMARY KEY AUTOINCREMENT, z TEXT)",
        "INSERT INTO test1 (x) VALUES ('XXX')",
        "INSERT INTO test2 (y) VALUES ('YYY')",
        "INSERT INTO test3 (z) VALUES ('ZZZ')"
    )
} catch (ex: BackingStoreException) {
    logger.error("An error occurred while executing SQL statements on the database.", ex)
}

Data Sanitization

When providing an SQL statement, use the standard SQL binding syntax. Use placeholders **?** for values to be inserted, updated, or used in WHERE clauses in SELECT statements.

Do not construct the SQL statement from variables like this:

    String sql = "INSERT INTO myTable VALUES (" + value1 + ", " + value2 + ", " + value3 + ")";

Instead, use the standard SQL binding syntax like this:

    String sql = "INSERT INTO myTable VALUES (?, ?, ?)";
    try {
        store.executeUpdate(sql, value1, value2, value3);
    } catch (BackingStoreException ex) {
        logger.error("An error occurred while executing a SQL statement.", ex);
    }

The ? character is recognized by SQLite as a placeholder for a value to be inserted. The execution methods all accept a representation of varargs which is essentially an array of arguments.

Key-Value Store

The SecureKeyValueStore is a key-value store based on a SecureDatabaseStore. It uses String as the key type, and can store any values of the following types:

  • Boolean
  • Byte
  • Double
  • Float
  • Integer
  • Long
  • Short
  • String
  • byte[]
  • Any Object that implements the Serializable interface

Creating and Opening the Key-Value Store

You must open the store with an encryption key before you can interact with it. The encryption key is used to encrypt the underlying persistence store.

SAP recommends that you use the Encryption utility to obtain the encryption key. If it is the first time the store is opened and null is provided as the encryption key, a key is generated transparently and is used for subsequent open calls. See Encryption Utility for more information.

The store fails to open if there are insufficient resources or permissions to open and/or create the store. The store can also fail to open if the encryption key is incorrect.

SecureKeyValueStore store = null;
try {
    store = new SecureKeyValueStore(context, "myKeyValueStoreName");
    final byte[] myEncryptionKey = EncryptionUtil.getEncryptionKey("aliasForMyKeyValueStore", myPasscode);
    store.open(myEncryptionKey);

    // Or passes null and the store will generate an encryption key which can be used
    // for subsequent open calls, e.g., store.open(null);
} catch (OpenFailureException ex) {
    logger.error("Failed to open key value store.", ex);
} catch (EncryptionError ex) {
    logger.error("Failed to get encryption key", ex);
}
var store: SecureKeyValueStore? = null
try {
    store = SecureKeyValueStore(context!!, "myKeyValueStoreName")
    val myEncryptionKey = EncryptionUtil.getEncryptionKey("aliasForMyKeyValueStore", myPasscode)
    store.open(myEncryptionKey)

    // Or passes null and the store will generate an encryption key which can be used
    // for subsequent open calls, e.g., store.open(null);
} catch (ex: OpenFailureException) {
    logger.error("Failed to open key value store.", ex)
} catch (ex: EncryptionError) {
    logger.error("Failed to get encryption key", ex)
}

Setting and Getting Values

A SecureKeyValueStore has <T> void put(String key, T value) to add values of the supported types, and methods to retrieve different data types in an appropriate format.

Put

class Organization implements Serializable {
    private String name;
    private String id;
    public Organization() { super();}
    public Organization(String name, String id) {
        this.name = name;
        this.id = id;
    }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
}

final String orgName = "Human Resource";
final String orgId = "ORG_HR_1";
Organization org = new Organization(orgName, orgId);

try {
    store.put(orgId, org);
} catch (BackingStoreException | TypeConversionException ex) {
    logger.error("Failed to add Organization {}", orgId, ex);
}
internal class Organization(var name: String?, var id: String?) : Serializable

val orgName = "Human Resource"
val orgId = "ORG_HR_1"
val org = Organization(orgName, orgId)

try {
    store!!.put(orgId, org)
} catch (ex: BackingStoreException) {
    logger.error("Failed to add Organization {}", orgId, ex)
} catch (ex: TypeConversionException) {
    logger.error("Failed to add Organization {}", orgId, ex)
}

Get

You must use the appropriate method to retrieve the data from the key value store by passing a String key

  • Integer getInt(String key)
  • Short getInt16(String key)
  • Long getInt64(String key)
  • Float getFloat(String key)
  • Double getDouble(String key)
  • byte[] getBlob(String key)
  • String getString(String key)
  • <T extends Serializable> T getSerializable(String key)
try {
    store.put("Integer value", 3);                  // Integer value.
    store.put("Double value", 3.5D);                // Double value.
    store.put("String value", "This is a String");  // String value.
    store.put("Int16Value", (short) 17);            // Short value.
    store.put("Int64Value", 41L);                   // Long value.
    store.put("FloatValue", 1.3F);                  // Float value.

    // Retrieves values with the appropriate get methods
// using the original keys.
Organization retrievedOrg = store.get(orgId);

    Integer intValue = store.getInt("Integer value");
    Double doubleValue = store.getDouble("Double value");
    String stringValue = store.getString("String value");
    Short shortValue = store.getInt16("Int16Value");
    Long longValue = store.getInt64("Int64Value");
    Float floatValue = store.getFloat("FloatValue");
} catch (BackingStoreException | TypeConversionException ex) {
    logger.error("An error occurred during put/get operation,", ex);
}
try {
    store!!.put("Integer value", 3)               // Integer value.
    store.put("Double value", 3.5)                // Double value.
    store.put("String value", "This is a String") // String value.
    store.put("Int16Value", 17.toShort())         // Short value.
    store.put("Int64Value", 41L)                  // Long value.
    store.put("FloatValue", 1.3f)                 // Float value.

    // Retrieves values with the appropriate get methods
    // using the original keys.
    val retrievedOrg = store.get<Organization>(orgId)

    val intValue = store.getInt("Integer value")
    val doubleValue = store.getDouble("Double value")
    val stringValue = store.getString("String value")
    val shortValue = store.getInt16("Int16Value")
    val longValue = store.getInt64("Int64Value")
    val floatValue = store.getFloat("FloatValue")
} catch (ex: BackingStoreException) {
    logger.error("An error occurred during put/get operation,", ex)
} catch (ex: TypeConversionException) {
    logger.error("An error occurred during put/get operation,", ex)
}

Getting Keys

You can use public String[] keys() to retrieve the keys of existing entries.

try {
    String[] keys = store.keys();

    for (String key : keys) {
        logger.debug("Retrieved key {}", key);
    }
} catch (FileClosedException | BackingStoreException ex) {
    logger.error("Failed to retrieve all existing keys: ", ex);
}
try {
    val keys = store!!.keys()

    for (key in keys) {
        logger.debug("Retrieved key {}", key)
    }
} catch (ex: FileClosedException) {
    logger.error("Failed to retrieve all existing keys: ", ex)
} catch (ex: BackingStoreException) {
    logger.error("Failed to retrieve all existing keys: ", ex)
}

Removing Entries

You can remove one or all existing entries in the store.

try {
    // Removes one entry using the key.
    store.remove("Int64Value");

    // Remove all existing entries.
    store.removeAll();
} catch (BackingStoreException ex) {
    logger.error("Failed to remove entry/entries.", ex);
}
try {
    // Removes one entry using the key.
    store!!.remove("Int64Value")

    // Remove all existing entries.
    store.removeAll()
} catch (ex: BackingStoreException) {
    logger.error("Failed to remove entry/entries.", ex)
}

Closing the Key-Value Store

When you finish executing key-value store operations, close the SecureKeyValueStore to relinquishes any resources it has acquired during its operations:

store.close();
store!!.close()

Preference Data Store

The SecurePreferenceDataStore provides an extra layer of application data protection by allowing you to replace the default SharedPreferences (file-based implementation).

This class is used with the Android Preference framework and extends the Android PreferenceDataStore in the android.support.v7.preference.PreferenceDataStore package.

This class uses the setPreferenceDataStore(PreferenceDataStore) method to replace the default SharedPreferences.

Creating and Opening the Preference Data Store

You must create the preference store with an encryption key before you can interact with it. The encryption key is used to encrypt and open the underlying persistence store.

SAP recommends that you use the Encryption utility to obtain the encryption key. If it is the first time the store is created and null is provided as the encryption key, a key is generated transparently and is used for subsequent persistence store opens. See Encryption Utility for more information.

The store fails to open if there are insufficient resources or permissions to open and/or create the store. The store can also fail to open if the encryption key is incorrect.

private static final String PREFERENCE_STORE_NAME = "myPreferenceStore";
private static final String PREFERENCE_STORE_ALIAS = "aliasForPreferenceStore";
...
EncryptionUtil.initialize(context);  // One time initialization
...

SecurePreferenceDataStore store = null;

try {
    final byte[] encryptionKey = EncryptionUtil.getEncryptionKey(
            PREFERENCE_STORE_ALIAS,
            myPasscode);
    store = new SecurePreferenceDataStore(
        context,              // Android application context
        PREFERENCE_STORE_NAME,// preference store name
        encryptionKey);// store encryption key; or passes null to use auto-generated encryption key.
} catch (OpenFailureException ex) {
    logger.error("An error occurred while opening the preference data store.", ex);
} catch (EncryptionError ex) {
    logger.error("Failed to get encryption key", ex);
}
companion object {
    private const val PREFERENCE_STORE_NAME = "myPreferenceStore"
    private const val PREFERENCE_STORE_ALIAS = "aliasForPreferenceStore"
}

...
EncryptionUtil.initialize(context!!) // One time initialization
...

var store: SecurePreferenceDataStore? = null

// Creating and opening the Preference Data Store.
try {
    val encryptionKey = EncryptionUtil.getEncryptionKey(
        PREFERENCE_STORE_ALIAS,
        myPasscode)
    store = SecurePreferenceDataStore(
        context,              // Android application context
        PREFERENCE_STORE_NAME,// preference store name
        encryptionKey)// store encryption key; or passes null to use auto-generated encryption key.
} catch (ex: OpenFailureException) {
    logger.error("An error occurred while opening the preference data store.", ex)
} catch (ex: EncryptionError) {
    logger.error("Failed to get encryption key", ex)
}

Adding and Retrieving Preferences

The following preference value types are supported:

  • String
  • Integer
  • Long
  • Float
  • Boolean
  • Any concrete Set class that is Serializable

You can use the appropriate put and get methods to add preference values to and retrieve them from the data store.

final String booleanKey = "BooleanKey";
final String floatKey = "FloatKey";
final String intKey = "IntKey";
final String longKey = "LongKey";
final String stringKey = "StringKey";

final boolean booleanValue = true;
final float floatValue = 1.3F;
final int intValue = 254;
final long longValue = Long.MAX_VALUE - 138;
final String stringValue = "A String value";

try {
    // Adds preferences of different types to the store.
    store.putInt(intKey, intValue);
    store.putString(stringKey, stringValue);
    store.putFloat(floatKey, floatValue);
    store.putLong(longKey, longValue);
    store.putBoolean(booleanKey, booleanValue);

    // Retrieves existing values.
    int retrievedIntValue = store.getInt(intKey, 0);
    String retrievedStringValue = store.getString(stringKey, "default String if not found");
    float retrievedFloatValue = store.getFloat(floatKey, 0.2F);
    long retrievedLongValue = store.getLong(longKey, 9223372036854775023L);
    boolean retrievedBooleanValue = store.getBoolean(booleanKey, false);

    // Adds a set of values as a preference to the store.
    final String stringSetKey = "StringSetKey";
    final String setValuePrefix = "StringValue";
    final int setSize = 20;
    HashSet<String> stringSet = new HashSet<>();

    for (int i = 0; i < setSize; i++) {
        stringSet.add(setValuePrefix + i);
    }

    store.putStringSet(stringSetKey, stringSet); // Adds a StringSet

    // Retrieves the StringSet.
    Set<String> retrievedStringSet = store.getStringSet(stringSetKey, null);

    if (retrievedStringSet != null) {
        for (int i = 0; i < setSize; i++) {
            assertTrue(retrievedStringSet.contains(setValuePrefix + i));
        }
    }
} catch (BackingStoreException ex) {
    logger.error("Failed to add/retrieve preference.", ex);
}
val booleanKey = "BooleanKey"
val floatKey = "FloatKey"
val intKey = "IntKey"
val longKey = "LongKey"
val stringKey = "StringKey"

val booleanValue = true
val floatValue = 1.3f
val intValue = 254
val longValue = java.lang.Long.MAX_VALUE - 138
val stringValue = "A String value"

try {
    // Adds preferences of different types to the store.
    store!!.putInt(intKey, intValue)
    store.putString(stringKey, stringValue)
    store.putFloat(floatKey, floatValue)
    store.putLong(longKey, longValue)
    store.putBoolean(booleanKey, booleanValue)

    // Retrieves existing values.
    val retrievedIntValue = store.getInt(intKey, 0)
    val retrievedStringValue = store.getString(stringKey, "default String if not found")
    val retrievedFloatValue = store.getFloat(floatKey, 0.2f)
    val retrievedLongValue = store.getLong(longKey, 9223372036854775023L)
    val retrievedBooleanValue = store.getBoolean(booleanKey, false)

    // Adds a set of values as a preference to the store.
    val stringSetKey = "StringSetKey"
    val setValuePrefix = "StringValue"
    val setSize = 20
    val stringSet = HashSet<String>()

    for (i in 0 until setSize) {
        stringSet.add(setValuePrefix + i)
    }

    store.putStringSet(stringSetKey, stringSet) // Adds a StringSet

    // Retrieves the StringSet.
    val retrievedStringSet = store.getStringSet(stringSetKey, null)

    if (retrievedStringSet != null) {
        for (i in 0 until setSize) {
            assertTrue(retrievedStringSet.contains(setValuePrefix + i))
        }
    }
} catch (ex: BackingStoreException) {
    logger.error("Failed to add/retrieve preference.", ex)
}

Removing Preferences

You can remove preferences one at a time or remove all at once:

try {
    // Removes preferences one at a time.
    store.remove(booleanKey);
    store.remove(floatKey);
    store.remove(intKey);
    store.remove(longKey);
    store.remove(stringKey);

    // Or removes all existing preferences.
    store.removeAll();
} catch (BackingStoreException ex) {
    logger.error("Failed to remove preference.", ex);
}
try {
    // Removes preferences one at a time.
    store!!.remove(booleanKey)
    store.remove(floatKey)
    store.remove(intKey)
    store.remove(longKey)
    store.remove(stringKey)

    // Or removes all existing preferences.
    store.removeAll()
} catch (ex: BackingStoreException) {
    logger.error("Failed to remove preference.", ex)
}

Closing the Preference Data Store

When you finish executing operations on the store, close the SecurePreferenceDataStore to relinquishes any resources it has acquired during its operations:

store.close();
store!!.close()

Last update: April 14, 2021