Skip to content

Encryption Utility

The EncryptionUtil class provides a protected storage for your mobile app users to generate and access an encryption key which is referenced using an alias. The encryption key can then be used with the Secure Store or any other place where an encryption key is needed.

Initialization

Before accessing encryption keys with EncryptionUtil, the app must initialize it with Android application context:

1
EncryptionUtil.initialize(context);
1
EncryptionUtil.initialize(context!!)

Encryption Key Generation Mechanisms

Depending on your requirements, you can generate an encryption key:

No Passcode

The application uses EncryptionUtil.getEncryptionKey(alias) to generate an encryption key without a passcode.

Since there is no passcode, EncryptionUtil generates a secondary encryption key randomly. This secondary encryption key is encrypted using the Secret Key generated from the Android Key Store and securely persisted. The secondary encryption key is returned to the caller.

To retrieve the encryption key:

1
2
3
4
5
6
7
8
9
try {

    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    byte[] key = EncryptionUtil.getEncryptionKey(myAlias);
} catch (EncryptionError ex) {
    logger.error("Failed to retrieve encryption key", ex);
}
1
2
3
4
5
6
7
8
9
try {

    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    val key = EncryptionUtil.getEncryptionKey(myAlias)
} catch (ex: EncryptionError) {
    logger.error("Failed to retrieve encryption key", ex)
}

Passcode

The application provides a passcode to EncryptionUtil.getEncryptionKey(alias, passcode), which then returns an encryption key. The encryption key is generated as follows:

  1. EncryptionUtil uses PBKDF2(Password-Based Key Derivation Function 2) to generate a Secret Key from the passcode.
  2. EncryptionUtil then generates a secondary key randomly.
  3. The secondary encryption key is then encrypted using the PBKDF2 secret key and securely persisted. This secondary encryption key is returned to the caller.

This is the most secure option because the secret key generated from the passcode is not stored on the device.

To retrieve the encryption key:

1
2
3
4
5
6
7
8
try {
    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    byte[] key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode);
} catch (EncryptionError ex) {
    logger.error("Failed to retrieve encryption key", ex);
}
1
2
3
4
5
6
7
8
try {
    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    val key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode)
} catch (ex: EncryptionError) {
    logger.error("Failed to retrieve encryption key", ex)
}

Passcode with Biometric

With this method, the application:

  1. Gets the initial cipher using EncryptionUtil.getCipher(alias).
  2. Passes the cipher to Android FingerprintManager for user authentication. This same cipher (user-authenticated) is passed back to EncryptionUtil.getEncryptionKey(alias, passcode, cipher).

The getEncryptionKey(alias, passcode, cipher) method generates a random secondary encryption key similar to the Passcode method, but encrypts it with the PBKDF2 secret key and separately with the cipher also. The two encrypted versions of the secondary encryption key are securely persisted. The secondary encryption key is returned to the caller. The secondary encryption key can later be retrieved using either getEncryptionKey(alias, passcode) or getEncryptionKey(alias, cipher).

To retrieve the encryption key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
try {
    byte[] key;

    // Gets a cipher first for FingerprintManager to authenticate the user.
    Cipher cipher = EncryptionUtil.getCipher(myAlias);

    // ...
    // The cipher is used by FingerprintManager to create a
    // CryptoObject during user authentication, then passed back.
    // ...

    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    // For new alias
    key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode, cipher);

    // Once in PASSCODE_BIOMETRIC state-- can retrieve the encryption
    // key with either passcode or cipher.
    key = EncryptionUtil.getEncryptionKey(myAlias, cipher);

    // -- Or --

    key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode);
} catch (EncryptionError ex) {
    logger.error("Failed to retrieve encryption key", ex);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var key: ByteArray
try {
    // Gets a cipher first for FingerprintManager to authenticate the user.
    val cipher = EncryptionUtil.getCipher(myAlias)

    // ...
    // The cipher is used by FingerprintManager to create a
    // CryptoObject during user authentication, then passed back.
    // ...

    // Do state transition if necessary;
    // see 'Encryption State Transitions' below for details.

    // For new alias
    key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode, cipher)

    // Once in PASSCODE_BIOMETRIC state-- can retrieve the encryption
    // key with either passcode or cipher.
    key = EncryptionUtil.getEncryptionKey(myAlias, cipher)

    // -- Or --

    key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode)
} catch (ex: EncryptionError) {
    logger.error("Failed to retrieve encryption key", ex)
}

Encryption State Transitions

In certain scenarios, the application switches the encryption key generation mechanism depending on the necessary application protection requirement. For example, it may switch from no passcode to passcode or from passcode to passcode with biometric.

To Passcode Only State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
try {
    byte[] key;
    EncryptionState state = EncryptionUtil.getState(myAlias);

    switch (state) {
        case INIT:
            // Enables PASSCODE_ONLY state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode);
            break;
        case NO_PASSCODE:
            EncryptionUtil.enablePasscode(myAlias, myPasscode);
            break;
        case PASSCODE_BIOMETRIC:
            EncryptionUtil.disableBiometric(myAlias, myPasscode);
            break;
        default:
            // Already in PASSCODE_ONLY state
            break;
    }

    // ...Retrieves the encryption key (for existing alias) with
    // EncryptionUti.getEncryptionKey(alias, passcode).

} catch (EncryptionError ex) {
    logger.error("Failed to transit to PASSCODE_ONLY state", ex);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
try {
    val state = EncryptionUtil.getState(myAlias)

    when (state) {
        INIT ->
            // Enables PASSCODE_ONLY state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode)
        NO_PASSCODE -> EncryptionUtil.enablePasscode(myAlias, myPasscode)
        PASSCODE_BIOMETRIC -> EncryptionUtil.disableBiometric(
                                myAlias, myPasscode)
        else -> {
            // Already in PASSCODE_ONLY state, do nothing
        }
    }

    // ...Retrieves the encryption key (for existing alias) with
    // EncryptionUti.getEncryptionKey(alias, passcode).

} catch (ex: EncryptionError) {
    logger.error("Failed to transit to PASSCODE_ONLY state", ex)
}

To Passcode with Biometric State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
try {
    // Gets a cipher first for FingerprintManager to authenticate the user.
    Cipher cipher = EncryptionUtil.getCipher(myAlias);

    // ...
    // The cipher is used by FingerprintManager to create a
    // CryptoObject during user authentication, then passed back.
    // ...

    EncryptionState state = EncryptionUtil.getState(myAlias);
    byte[] key;

    switch (state) {
        case INIT:
            // Enables PASSCODE_BIOMETRIC state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode, cipher);
            break;
        case NO_PASSCODE: // Falls through
        case PASSCODE_ONLY:
            EncryptionUtil.enableBiometric(myAlias, myPasscode, cipher);
            break;
        default: // Already in PASSCODE_BIOMETRIC state
            break;
    }

    // ...Retrieves the encryption key (for existing alias) with
    // EncryptionUti.getEncryptionKey(alias, passcode)
    //    or EncryptionUti.getEncryptionKey(alias, cipher);
    // remember to precede those method calls with getCipher(alias).
} catch (EncryptionError ex) {
    logger.error("Failed to transit to PASSCODE_BIOMETRIC state", ex);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
try {
    // Gets a cipher first for FingerprintManager to authenticate the user.
    val cipher = EncryptionUtil.getCipher(myAlias)

    // ...
    // The cipher is used by FingerprintManager to create a
    // CryptoObject during user authentication, then passed back.
    // ...

    val state = EncryptionUtil.getState(myAlias)

    when (state) {
        INIT ->
            // Enables PASSCODE_BIOMETRIC state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode, cipher)
        NO_PASSCODE, PASSCODE_ONLY -> EncryptionUtil.enableBiometric(
                                        myAlias, myPasscode, cipher)
        else -> {
            // Already in PASSCODE_BIOMETRIC state, do nothing
        }
    }

    // ...Retrieves the encryption key (for existing alias) with
    // EncryptionUti.getEncryptionKey(alias, passcode)
    //    or EncryptionUti.getEncryptionKey(alias, cipher);
    // remember to precede those method calls with getCipher(alias).
} catch (ex: EncryptionError) {
    logger.error("Failed to transit to PASSCODE_BIOMETRIC state", ex)
}

To No Passcode State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
try {
    byte[] key;
    EncryptionState state = EncryptionUtil.getState(myAlias);

    switch (state) {
        case INIT:
            // Enables NO_PASSCODE state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias);
            break;
        case PASSCODE_ONLY: // Falls through
        case PASSCODE_BIOMETRIC:
            EncryptionUtil.disablePasscode(myAlias, myPasscode);
            break;
        default: // Already in NO_PASSCODE state
            break;
    }

    // ...Retrieves the encryption key with
    // EncryptionUti.getEncryptionKey(alias) from this point on.
} catch (EncryptionError ex) {
    logger.error("Failed to transit to NO_PASSCODE state", ex);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
try {
    val state = EncryptionUtil.getState(myAlias)

    when (state) {
        INIT ->
            // Enables NO_PASSCODE state for new alias
            key = EncryptionUtil.getEncryptionKey(myAlias)
        PASSCODE_ONLY, PASSCODE_BIOMETRIC -> EncryptionUtil.disablePasscode(
                                                myAlias, myPasscode)
        else -> {
            // Already in NO_PASSCODE state
        }
    }

    // ...Retrieves the encryption key with
    // EncryptionUti.getEncryptionKey(alias) from this point on.
} catch (ex: EncryptionError) {
    logger.error("Failed to transit to NO_PASSCODE state", ex)
}

Changing Passcode

The application can change the passcode using EncryptionUtil.changePasscode(alias, oldPasscode, newPasscode) when in Passcode Only or Passcode with Biometric state. Once the old passcode is verified, the new passcode is used to encrypt the original encryption key.

1
2
3
4
5
6
7
try {
    EncryptionUtil.changePasscode(myAlias, myPasscode, myNewPasscode);
} catch (EncryptionError ex) {
    // The old passcode is not correct; get it again from the end user
    // then retry changePasscode...

}
1
2
3
4
5
6
7
try {
    EncryptionUtil.changePasscode(myAlias, myPasscode, myNewPasscode)
} catch (ex: EncryptionError) {
    // The old passcode is not correct; get it again from the end user
    // then retry changePasscode...

}

Backup Considerations

For security reason, the foundation module has android:allowBackup="false" in its manifest file.

Also, the encryption keys obtained in No Passcode State or Passcode with Biometric State rely on Android KeyStore and cannot be backed up/restored. Therefore, any Secure Store-- SecureDatabaseStore, SecureKeyValueStore, SecureStoreCache, SecurePreferenceDataStore, or AppUsage store, that uses such encryption key cannot not be backed up/restored.

In case you want to enable backup for the application, the application must exclude the shared preferences files and Secure Store databases associated with those encryption keys with custom backup rules by following the steps below:

  1. Enabling Backup-- Because foundation module has android:allowBackup="false" in its manifest file, you must use Android merge rule marker to override that attribute. See Android Studio User Guide-- Merge multiple manifest files for more details.

    Warning

    Just having android:allowBackup="true" in your app's AndroidManifest.xml file will fail the application build with Manifest merger failed error.

    • Add xmlns:tools="http://schemas.android.com/tools" to <manifest> element

    • Add the two attributes in <application> element:

      android:allowBackup="true"

      tools:replace="android:allowBackup"

  2. Creating Backup Rules File-- In AndroidManifest.xml, add android:fullBackupContent attribute in <application> element. This attribute points to an XML file under res/xml folder that holds customized backup rules.

    For example,

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.sap.android.assistant">
    
        <application ...
            android:allowBackup="true"
            ...
            android:fullBackupContent="@xml/backup_descriptor"
            tools:replace="android:allowBackup">
    
            <activity ...
        </application>
        ...
    </manifest>
    
  3. Adding Backup Rules-- Create an XML file in the res/xml folder specified by android:fullBackupContent attribute. Inside the backup rule file, add <exclude> elements to exclude files for the sharedpref and database domains:

    • <exclude domain="sharedpref" path="<encryption key alias>_sharedPreference##.xml">

    • <exclude domain="database" path="<the name of the secure store that uses the above encryption key>">

    For example,

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="utf-8"?>
    <full-backup-content>
        <!--
        1. Exclude shared preferences in the pattern of "encryptionKeyAlias"
           appended with "_sharedPreference##.xml" for all encryption keys
           in No Passcode State or Passcode with Biometric State.
    
        2. Exclude secure stores using those encryption keys-- in this example,
            Secure store "APP_SECURE_STORE" is using encryption key with alias "app_pc_alias".
            Secure store "RLM_SECURE_STORE" is using encryption key with alias "rlm_pc_alias".
        -->
    
        <exclude domain="sharedpref" path="app_pc_alias_sharedPreference##.xml" />
        <exclude domain="sharedpref" path="rlm_pc_alias_sharedPreference##.xml" />
        <exclude domain="database" path="APP_SECURE_STORE" />
        <exclude domain="database" path="RLM_SECURE_STORE" />
    </full-backup-content>