Class EncryptionUtil

  • All Implemented Interfaces:

    
    public class EncryptionUtil
    
                        

    Provides a protected storage for the users to generate and access their encryption key associated with an alias. The encryption key can then be used with Secure Store and any other place where encryption key is needed.

    Initialization

    Before accessing encryption keys with EncryptionUtil, it is required for the app to initialize it with Android application context.

    For example,

    
        EncryptionUtil.initialize(context);
    

    Encryption Key Generation Mechanisms

    1. Passcode

    The application would give the passcode to getEncryptionKey, which would return an encryption key by first using PBKDF2 (Password-Based Key Derivation Function 2) to generate a Secret Key from the passcode and then generating a secondary key randomly. This secondary encryption key is then encrypted using the PBKDF2 secret key and securely persisted. The 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,

    
        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);
        }
    

    2. Passcode with Biometric

    The application would:

    • Get the initial cipher via getCipher.
    • Pass the cipher to Android FingerprintManager for user authentication.
    • The same cipher (user-authenticated) is passed back to getEncryptionKey.

    The getEncryptionKey(alias, passcode, cipher) method will generate a random secondary encryption key similar to Passcode mechanism, but will encrypt 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 or getEncryptionKey.

    To retrieve the encryption key,

    
        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.
    
            key = EncryptionUtil.getEncryptionKey(myAlias, myPasscode, cipher); // For new alias
    
            // 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);
        }
    

    3. No Passcode

    The application would use getEncryptionKey to generate an encryption key without the passcode.

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

    To retrieve the encryption key,

    
        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);
        }
    
    Encryption State Transitions

    There are scenarios that the application would switch the encryption key generation mechanism depending on the necessary application protection requirement. For example, from no passcode to passcode, or from passcode to passcode with biometric, etc.

    1. To Passcode Only State

    
        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);
        }
    

    2. To Passcode with Biometric State

    
        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);
        }
    

    3. To No Passcode State

    
        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);
        }
    
    To Change Passcode

    The application can change the passcode using changePasscode 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.

    
        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...
        }
    

    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. 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 by defining custom XML rules.

    1. Enable Backup-- Because foundation module has android:allowBackup="false" in its manifest file, you need to use Android merge rule marker to override the attribute.

    a. Add xmlns:tools="http://schemas.android.com/tools" to <manifest>
       element
    
    b. Enable the backup by adding the two attributes in <application> element:
         android:allowBackup="true"
         tools:replace="android:allowBackup"
    

    2. Create 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 contains backup rules.

    For example,

    
        <?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. Add 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="<secure store name>" />
    

    For example,

    
        <?xml version="1.0" encoding="utf-8"?>
        <full-backup-content>
            <!--
            1. Exclude shared preferences in the pattern of "encryptionKeyAlias" followed by
                "_sharedPreference##.xml" for all encryption keys in No Passcode State or Passcode with
                Biometric State.
    
            2. Exclude secure stores using those encryption keys--
                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>
    
    • Nested Class Summary

      Nested Classes 
      Modifier and Type Class Description
    • Field Summary

      Fields 
      Modifier and Type Field Description
    • Constructor Summary

      Constructors 
      Constructor Description
    • Enum Constant Summary

      Enum Constants 
      Enum Constant Description
    • Method Summary

      Modifier and Type Method Description
      static void initialize(@NonNull() Context context) Initializes with Android application context.
      static Array<byte> getEncryptionKey(@NonNull() String alias) Generates an encryption key for the new alias, or returns the existing encryption key when in NO_PASSCODE state.
      static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Array<char> passcode) Returns the encryption key when in PASSCODE_ONLY or PASSCODE_BIOMETRIC state for an existing alias.
      static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Array<char> passcode, @NonNull() Cipher cipher) This method typically is used to generate a new encryption key for a new alias and enters PASSCODE_BIOMETRIC state.
      static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Cipher cipher) Returns the existing encryption key when in PASSCODE_BIOMETRIC state.
      static void changePasscode(@NonNull() String alias, @NonNull() Array<char> oldPasscode, @NonNull() Array<char> newPasscode) Changes the passcode so that the user's key is re-encrypted with the new passcode when in PASSCODE_ONLY or PASSCODE_BIOMETRIC state.
      static void enablePasscode(@NonNull() String alias, @NonNull() Array<char> passcode) When in NO_PASSCODE state, enables passcode so that the user's key is re-encrypted with the provided passcode.
      static void enableBiometric(@NonNull() String alias, @NonNull() Array<char> passcode, @NonNull() Cipher cipher) When in NO_PASSCODE or PASSCODE_ONLY state, re-encrypted the encryption key with the passcode and biometric authentication.
      static void disablePasscode(@NonNull() String alias, @NonNull() Array<char> existingPasscode) Disables the passcode and biometric authentication when in PASSCODE_ONLY or PASSCODE_BIOMETRIC.
      static void disablePasscode(@NonNull() String alias, @NonNull() Cipher cipher) Re-encrypts user's key without the passcode and cipher for biometric authentication.
      static void disableBiometric(@NonNull() String alias, @NonNull() Array<char> existingPasscode) Disables the biometric authentication and the encryption key can only be retrieved via getEncryptionKey only after this method returns successfully.
      static void changeIterationCount(@NonNull() String alias, @NonNull() Array<char> passcode, @IntRange(from = 1000) int iterationCount) An encryption key that requires passcode (in PASSCODE_ONLY or PASSCODE_BIOMETRIC state) uses PBKDF2 to generate a Secret Key from the passcode and then generating a secondary key randomly.
      static void delete(@NonNull() String alias) Removes the encryption key.
      static Cipher getCipher(@NonNull() String alias) Obtains a cipher object for Android FingerprintManager to derive a crypto object for authentication, then passes back to one of the methods-- enableBiometric, getEncryptionKey, or getEncryptionKey.
      static EncryptionState getState(@NonNull() String alias) Returns the current encryption state.
      • Methods inherited from class java.lang.Object

        clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    • Constructor Detail

    • Method Detail

      • initialize

         static void initialize(@NonNull() Context context)

        Initializes with Android application context. This is the first method to call before using other EncryptionUtil methods.

        Parameters:
        context - Android application context
      • getEncryptionKey

        @NonNull() static Array<byte> getEncryptionKey(@NonNull() String alias)

        Generates an encryption key for the new alias, or returns the existing encryption key when in NO_PASSCODE state.

        Note:

        An encryption key in NO_PASSCODE state cannot be backed up/restored, the associated shared preference file (<your encryption key alias>__sharedPreference##.xml) and Secure Store database must be excluded from the application's <full-backup-content> element in the res/xml file specified by the <application> attribute android:fullBackupContent in AndroidManifest.xml.

        Parameters:
        alias - the alias of the encryption key
      • getEncryptionKey

        @NonNull() static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Array<char> passcode)

        Returns the encryption key when in PASSCODE_ONLY or PASSCODE_BIOMETRIC state for an existing alias.

        For a new alias, this method will create an encryption key in PASSCODE_ONLY state.

        Parameters:
        alias - the alias of the encryption key
        passcode - the authentication passcode
      • getEncryptionKey

        @NonNull() static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Array<char> passcode, @NonNull() Cipher cipher)

        This method typically is used to generate a new encryption key for a new alias and enters PASSCODE_BIOMETRIC state.

        When getting an existing key, calling getEncryptionKey with either a passcode (getEncryptionKey) or a cipher ((String, Cipher)) will suffice.

        Note:

        An encryption key in PASSCODE_BIOMETRIC state cannot be backed up/restored, the associated shared preference file (<your encryption key alias>__sharedPreference##.xml) and Secure Store database must be excluded from the application's <full-backup-content> element in the res/xml file specified by the <application> attribute android:fullBackupContent in AndroidManifest.xml.

        Parameters:
        alias - the alias of the encryption key
        passcode - the authentication passcode
        cipher - the cipher returned from getCipher
      • getEncryptionKey

        @NonNull() static Array<byte> getEncryptionKey(@NonNull() String alias, @NonNull() Cipher cipher)

        Returns the existing encryption key when in PASSCODE_BIOMETRIC state.

        For a new alias, the app must call enableBiometric first to move to PASSCODE_BIOMETRIC state before calling this method.

        Note:

        An encryption key in PASSCODE_BIOMETRIC state cannot be backed up/restored, the associated shared preference file (<your encryption key alias>__sharedPreference##.xml) and Secure Store database must be excluded from the application's <full-backup-content> element in the res/xml file specified by the <application> attribute android:fullBackupContent in AndroidManifest.xml.

        Parameters:
        alias - the alias of the encryption key
        cipher - the cipher returned from getCipher
      • changePasscode

         static void changePasscode(@NonNull() String alias, @NonNull() Array<char> oldPasscode, @NonNull() Array<char> newPasscode)

        Changes the passcode so that the user's key is re-encrypted with the new passcode when in PASSCODE_ONLY or PASSCODE_BIOMETRIC state.

        Parameters:
        alias - the alias of the encryption key
        oldPasscode - the old passcode
        newPasscode - the new passcode
      • enablePasscode

         static void enablePasscode(@NonNull() String alias, @NonNull() Array<char> passcode)

        When in NO_PASSCODE state, enables passcode so that the user's key is re-encrypted with the provided passcode. Now the encryption key is in PASSCODE_ONLY state and can be retrieved via getEncryptionKey.

        Parameters:
        alias - the alias of the encryption key
        passcode - the passcode
      • enableBiometric

         static void enableBiometric(@NonNull() String alias, @NonNull() Array<char> passcode, @NonNull() Cipher cipher)

        When in NO_PASSCODE or PASSCODE_ONLY state, re-encrypted the encryption key with the passcode and biometric authentication.

        Parameters:
        alias - the alias of the encryption key
        passcode - the existing passcode when in PASSCODE_ONLY state or a new passcode when in NO_PASSCODE state
        cipher - the cipher returned from getCipher
      • disablePasscode

         static void disablePasscode(@NonNull() String alias, @NonNull() Array<char> existingPasscode)

        Disables the passcode and biometric authentication when in PASSCODE_ONLY or PASSCODE_BIOMETRIC. The user's key can only be retrieved via getEncryptionKey after this method returns successfully.

        The app must call enablePasscode to move to PASSCODE_ONLY state, or enableBiometric to move to PASSCODE_BIOMETRIC state.

        Parameters:
        alias - the alias of the encryption key
        existingPasscode - the original passcode
      • disablePasscode

         static void disablePasscode(@NonNull() String alias, @NonNull() Cipher cipher)

        Re-encrypts user's key without the passcode and cipher for biometric authentication. The user's key can be retrieved via getEncryptionKey after this method returns successfully. getEncryptionKey will fail, because biometric authentication must co-exist with passcode.

        Note that if the user calls getEncryptionKey after this method returns, the user's key will be protected by the passcode and biometric authentication again.

        Parameters:
        alias - the alias of the encryption key
        cipher - the cipher returned from getCipher
      • disableBiometric

         static void disableBiometric(@NonNull() String alias, @NonNull() Array<char> existingPasscode)

        Disables the biometric authentication and the encryption key can only be retrieved via getEncryptionKey only after this method returns successfully.

        Note that if the user calls getEncryptionKey, the user's encryption key will be protected by biometric authentication again.

        Parameters:
        alias - the alias of the encryption key
        existingPasscode - the existing passcode
      • changeIterationCount

         static void changeIterationCount(@NonNull() String alias, @NonNull() Array<char> passcode, @IntRange(from = 1000) int iterationCount)

        An encryption key that requires passcode (in PASSCODE_ONLY or PASSCODE_BIOMETRIC state) uses PBKDF2 to generate a Secret Key from the passcode and then generating a secondary key randomly. This secondary encryption key is then encrypted using the PBKDF2 secret key and securely persisted, this secondary encryption key is returned to the caller of getEncryptionKey methods.

        By default, the iteration count used by the PBKDF2 Secret Key mechanism is 1000. Call this method to increase the iteration count for an existing encryption key associated with a passcode. The improvement in security is gained by slowing performance. The higher the iteration count the longer it takes to compute the key from the passcode. An offline attacker trying to guess the passcode has to do that computation for each passcode they guess.

        Once this method is called, the subsequent calls to getEncryptionKey or changePasscode will use the same iteration count.

        Parameters:
        alias - the alias of the encryption key
        passcode - the authentication passcode
        iterationCount - at least {@value EncryptionHelper#MIN_ITERATION_COUNT} (the default).
      • delete

         static void delete(@NonNull() String alias)

        Removes the encryption key. Any data encrypted with the original encryption key will no longer be decryptable. This method is typically called when the user forgot the passcode.

        Parameters:
        alias - the alias of the encryption key
      • getCipher

        @NonNull() static Cipher getCipher(@NonNull() String alias)

        Obtains a cipher object for Android FingerprintManager to derive a crypto object for authentication, then passes back to one of the methods-- enableBiometric, getEncryptionKey, or getEncryptionKey. Those methods will fail if the input cipher is not obtained via this method.

        Parameters:
        alias - the alias of the encryption key
      • getState

        @NonNull() static EncryptionState getState(@NonNull() String alias)

        Returns the current encryption state.

        Parameters:
        alias - the alias of the encryption key