diff --git a/docs/Migration-from-1.6-to-1.7.md b/docs/Migration-from-1.6-to-1.7.md index 7d08162e..29d68a1a 100644 --- a/docs/Migration-from-1.6-to-1.7.md +++ b/docs/Migration-from-1.6-to-1.7.md @@ -214,3 +214,12 @@ The behavior of `PowerAuthSDK.authenticateUsingBiometry()` has been slightly cha ### tvOS The `PowerAuthSDK.authenticateUsingBiometry()` function is no longer available on tvOS platform. + +## Changes in 1.7.10+ + +### Android + +- The shared biometry-related encryption key is no longer supported in `PowerAuthSDK`. If an activation is already using the shared key, then it's in use until the activation or the biometry factor is removed. As part of this change, the following methods are now deprecated: + - Method `PowerAuthSDK.removeActivationLocal(Context, boolean)` is now deprecated. Use `removeActivationLocal(Context)` as a replacement. + - Method `PowerAuthKeychainConfiguration.getKeychainBiometryDefaultKey()` is now deprecated. Use `getKeychainKeyBiometry()` as a replacement. + - Method `PowerAuthKeychainConfiguration.Builder.keychainBiometryDefaultKey(String)` is now deprecated. Use `keychainKeyBiometry(String)` as a replacement. diff --git a/docs/Migration-from-1.7-to-1.8.md b/docs/Migration-from-1.7-to-1.8.md index 4a014710..8dc174fb 100644 --- a/docs/Migration-from-1.7-to-1.8.md +++ b/docs/Migration-from-1.7-to-1.8.md @@ -297,4 +297,14 @@ You can watch the following related issues: - [wultra/powerauth-mobile-sdk#551](https://github.com/wultra/powerauth-mobile-sdk/issues/551) - [wultra/powerauth-mobile-watch-sdk#7](https://github.com/wultra/powerauth-mobile-watch-sdk/issues/7) -- [wultra/powerauth-mobile-extensions-sdk#7](https://github.com/wultra/powerauth-mobile-extensions-sdk/issues/7) \ No newline at end of file +- [wultra/powerauth-mobile-extensions-sdk#7](https://github.com/wultra/powerauth-mobile-extensions-sdk/issues/7) + +## Changes in 1.8.3+ + +### Android + +- The shared biometry-related encryption key is no longer supported in `PowerAuthSDK`. If an activation is already using the shared key, then it's in use until the activation or the biometry factor is removed. As part of this change, the following methods are now deprecated: + - Method `PowerAuthSDK.removeActivationLocal(Context, boolean)` is now deprecated. Use `removeActivationLocal(Context)` as a replacement. + - Method `PowerAuthKeychainConfiguration.getKeychainBiometryDefaultKey()` is now deprecated. Use `getKeychainKeyBiometry()` as a replacement. + - Method `PowerAuthKeychainConfiguration.Builder.keychainBiometryDefaultKey(String)` is now deprecated. Use `keychainKeyBiometry(String)` as a replacement. + \ No newline at end of file diff --git a/docs/Migration-from-1.8-to-1.9.md b/docs/Migration-from-1.8-to-1.9.md index 249cc57c..9cdcd60b 100644 --- a/docs/Migration-from-1.8-to-1.9.md +++ b/docs/Migration-from-1.8-to-1.9.md @@ -20,6 +20,11 @@ PowerAuth Mobile SDK in version `1.9.0` provides the following improvements: - Synchronous method `getEciesEncryptorForApplicationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. - Synchronous method `getEciesEncryptorForActivationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. +- The shared biometry-related encryption key is no longer supported in `PowerAuthSDK`. If an activation is already using the shared key, then it's in use until the activation or the biometry factor is removed. As part of this change, the following methods are now deprecated: + - Method `PowerAuthSDK.removeActivationLocal(Context, boolean)` is now deprecated. Use `removeActivationLocal(Context)` as a replacement. + - Method `PowerAuthKeychainConfiguration.getKeychainBiometryDefaultKey()` is now deprecated. Use `getKeychainKeyBiometry()` as a replacement. + - Method `PowerAuthKeychainConfiguration.Builder.keychainBiometryDefaultKey(String)` is now deprecated. Use `keychainKeyBiometry(String)` as a replacement. + - Removed all interfaces deprecated in release `1.8.x` ### Other changes diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthTestHelper.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthTestHelper.java index 3af4a5a2..7edbecc4 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthTestHelper.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthTestHelper.java @@ -208,7 +208,7 @@ public Builder(@NonNull Context context, @NonNull PowerAuthTestConfig testConfig if (sdk.hasValidActivation()) { Logger.e("Shared PowerAuthSDK has a valid activation at test initialization."); } - sdk.removeActivationLocal(context, true); + sdk.removeActivationLocal(context); } else { if (!sdk.hasValidActivation()) { Logger.e("Shared PowerAuthSDK doesn't have a valid activation at test initialization."); @@ -449,7 +449,7 @@ private PowerAuthTestHelper( .keychainConfiguration(getSharedPowerAuthKeychainConfiguration()) .build(getContext()); if (resetActivation && sdk.hasValidActivation()) { - sdk.removeActivationLocal(getContext(), true); + sdk.removeActivationLocal(getContext()); } return sdk; } diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/ActivationHelper.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/ActivationHelper.java index c3ec26d0..72f3da8f 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/ActivationHelper.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/ActivationHelper.java @@ -188,17 +188,15 @@ public void removeActivation(boolean revokeRecoveryCodes) throws Exception { invalidAuthentication = null; createActivationResult = null; } - removeActivationLocal(true); + removeActivationLocal(); } /** * Remove activation locally. - * - * @param removeSharedBiometryKey If true, then also remove a shared biometry key. */ - public void removeActivationLocal(boolean removeSharedBiometryKey) { + public void removeActivationLocal() { if (powerAuthSDK.hasValidActivation()) { - powerAuthSDK.removeActivationLocal(testHelper.getContext(), removeSharedBiometryKey); + powerAuthSDK.removeActivationLocal(testHelper.getContext()); } } diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java index f15eb814..be91afe9 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java @@ -254,7 +254,7 @@ public void onActivationCreateFailed(@NonNull Throwable t) { public void testRemoveActivationLocal() throws Exception { activationHelper.createStandardActivation(true, null); // Remove activation - powerAuthSDK.removeActivationLocal(testHelper.getContext(), true); + powerAuthSDK.removeActivationLocal(testHelper.getContext()); // Back to Initial expectations assertFalse(powerAuthSDK.hasValidActivation()); assertFalse(powerAuthSDK.hasPendingActivation()); diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfigurationBuilderTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfigurationBuilderTest.java index aadc7935..2554b6f2 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfigurationBuilderTest.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfigurationBuilderTest.java @@ -35,7 +35,7 @@ public void testDefaultParameters() throws Exception { assertEquals(PowerAuthKeychainConfiguration.KEYCHAIN_ID_BIOMETRY, configuration.getKeychainBiometryId()); assertEquals(PowerAuthKeychainConfiguration.KEYCHAIN_ID_STATUS, configuration.getKeychainStatusId()); assertEquals(PowerAuthKeychainConfiguration.KEYCHAIN_ID_TOKEN_STORE, configuration.getKeychainTokenStoreId()); - assertEquals(PowerAuthKeychainConfiguration.KEYCHAIN_KEY_BIOMETRY_DEFAULT, configuration.getKeychainBiometryDefaultKey()); + assertNull(configuration.getKeychainKeyBiometry()); assertEquals(KeychainProtection.NONE, configuration.getMinimalRequiredKeychainProtection()); assertFalse(configuration.isConfirmBiometricAuthentication()); assertTrue(configuration.isLinkBiometricItemsToCurrentSet()); @@ -50,14 +50,14 @@ public void testCustomParameters() throws Exception { .keychainBiometryId("keychain.biometry") .keychainStatusId("keychain.status") .keychainTokenStoreId("keychain.tokens") - .keychainBiometryDefaultKey("biometryKey") + .keychainKeyBiometry("biometryKey") .minimalRequiredKeychainProtection(KeychainProtection.HARDWARE) .authenticateOnBiometricKeySetup(false) .build(); assertEquals("keychain.biometry", configuration.getKeychainBiometryId()); assertEquals("keychain.status", configuration.getKeychainStatusId()); assertEquals("keychain.tokens", configuration.getKeychainTokenStoreId()); - assertEquals("biometryKey", configuration.getKeychainBiometryDefaultKey()); + assertEquals("biometryKey", configuration.getKeychainKeyBiometry()); assertEquals(KeychainProtection.HARDWARE, configuration.getMinimalRequiredKeychainProtection()); assertTrue(configuration.isConfirmBiometricAuthentication()); assertFalse(configuration.isLinkBiometricItemsToCurrentSet()); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java index 3d47da62..44597b9d 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java @@ -135,7 +135,7 @@ public void onCompletion() { @Override public void onBiometricKeyUnavailable() { // Remove the default key, because the biometric key is no longer available. - device.getBiometricKeystore().removeBiometricKeyEncryptor(); + device.getBiometricKeystore().removeBiometricKeyEncryptor(request.getKeystoreAlias()); } }); final IBiometricKeyEncryptorProvider biometricKeyEncryptorProvider = new DefaultBiometricKeyEncryptorProvider(request, getBiometricKeystore()); @@ -170,7 +170,7 @@ public void onBiometricKeyUnavailable() { } // Failed to use biometric authentication. At first, we should cleanup the possible stored // biometric key. - device.getBiometricKeystore().removeBiometricKeyEncryptor(); + device.getBiometricKeystore().removeBiometricKeyEncryptor(request.getKeystoreAlias()); // Now show the error dialog, and report the exception later. if (exception == null) { diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthenticationRequest.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthenticationRequest.java index 0c2d5b47..29fbc2e0 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthenticationRequest.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthenticationRequest.java @@ -38,6 +38,7 @@ public class BiometricAuthenticationRequest { private final @NonNull CharSequence title; private final @Nullable CharSequence subtitle; private final @NonNull CharSequence description; + private final @NonNull String keystoreAlias; private final boolean forceGenerateNewKey; private final boolean invalidateByBiometricEnrollment; private final boolean userConfirmationRequired; @@ -52,6 +53,7 @@ private BiometricAuthenticationRequest( @NonNull CharSequence description, @Nullable Fragment fragment, @Nullable FragmentActivity fragmentActivity, + @NonNull String keystoreAlias, boolean forceGenerateNewKey, boolean invalidateByBiometricEnrollment, boolean userConfirmationRequired, @@ -64,6 +66,7 @@ private BiometricAuthenticationRequest( this.description = description; this.fragment = fragment; this.fragmentActivity = fragmentActivity; + this.keystoreAlias = keystoreAlias; this.forceGenerateNewKey = forceGenerateNewKey; this.invalidateByBiometricEnrollment = invalidateByBiometricEnrollment; this.userConfirmationRequired = userConfirmationRequired; @@ -108,6 +111,14 @@ private BiometricAuthenticationRequest( return fragmentActivity; } + /** + * @return Alias to Android Keystore for the existing, or the new created key. + */ + @NonNull + public String getKeystoreAlias() { + return keystoreAlias; + } + /** * @return true whether the new biometric key has to be generated as a part of the operation. */ @@ -173,6 +184,7 @@ public static class Builder { private Fragment fragment; private FragmentActivity fragmentActivity; + private String keystoreAlias; private boolean forceGenerateNewKey = false; private boolean invalidateByBiometricEnrollment = true; private boolean userConfirmationRequired = false; @@ -200,6 +212,9 @@ public BiometricAuthenticationRequest build() { if (TextUtils.isEmpty(title) || TextUtils.isEmpty(description)) { throw new IllegalArgumentException("Title and description is required."); } + if (keystoreAlias == null) { + throw new IllegalArgumentException("KeyStore alias is required."); + } if (rawKeyData == null) { throw new IllegalArgumentException("RawKeyData is required."); } @@ -218,6 +233,7 @@ public BiometricAuthenticationRequest build() { description, fragment, fragmentActivity, + keystoreAlias, forceGenerateNewKey, invalidateByBiometricEnrollment, userConfirmationRequired, @@ -317,6 +333,16 @@ public Builder setFragmentActivity(@NonNull FragmentActivity fragmentActivity) { return this; } + /** + * Required: Set alias for a new or existing key stored in the Android Keystore. + * @param keystoreAlias Alias to key to create or access. + * @return This value will never be {@code null}. + */ + public Builder setKeystoreAlias(@NonNull String keystoreAlias) { + this.keystoreAlias = keystoreAlias; + return this; + } + /** * @param forceGenerateNewKey If true then the new biometric key will be generated as a * part of the process. diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricKeystore.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricKeystore.java index 856f96a0..e869ea5f 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricKeystore.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricKeystore.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.biometry; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** @@ -34,11 +35,12 @@ public interface IBiometricKeystore { /** * Check if a key for biometric key encryptor is present in Keystore and {@link IBiometricKeyEncryptor} * can be acquired. + * @param keyId Key identifier. * * @return {@code true} in case a key for biometric key encryptor is present, false otherwise. * Method returns false in case Keystore is not properly initialized (call {@link #isKeystoreReady()}). */ - boolean containsBiometricKeyEncryptor(); + boolean containsBiometricKeyEncryptor(@NonNull String keyId); /** * Generate a new biometry related Keystore key and return object that provide KEK encryption and decryption. @@ -48,21 +50,32 @@ public interface IBiometricKeystore { * * @param invalidateByBiometricEnrollment Sets whether the new key should be invalidated on biometric enrollment. * @param useSymmetricKey Sets whether symmetric key should be created. + * @param keyId Key identifier. * * @return New generated {@link IBiometricKeyEncryptor} key or {@code null} in case of failure. */ @Nullable - IBiometricKeyEncryptor createBiometricKeyEncryptor(boolean invalidateByBiometricEnrollment, boolean useSymmetricKey); + IBiometricKeyEncryptor createBiometricKeyEncryptor(@NonNull String keyId, boolean invalidateByBiometricEnrollment, boolean useSymmetricKey); /** * Removes an encryption key from Keystore. + * @param keyId Key identifier. */ - void removeBiometricKeyEncryptor(); + void removeBiometricKeyEncryptor(@NonNull String keyId); /** + * Get implementation of {@link IBiometricKeyEncryptor} constructed with key stored in KeyStore. + * @param keyId Key identifier. * @return {@link IBiometricKeyEncryptor} constructed with key stored in KeyStore or {@code null} * if no such key is stored. */ @Nullable - IBiometricKeyEncryptor getBiometricKeyEncryptor(); + IBiometricKeyEncryptor getBiometricKeyEncryptor(@NonNull String keyId); + + /** + * Return identifier of legacy key shared between multiple PowerAuthSDK instances. + * @return Identifier of shared legacy key. + */ + @NonNull + String getLegacySharedKeyId(); } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricKeystore.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricKeystore.java index 4e00d129..6048de41 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricKeystore.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricKeystore.java @@ -17,10 +17,13 @@ package io.getlime.security.powerauth.biometry.impl; import android.os.Build; +import android.util.Base64; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; @@ -33,6 +36,7 @@ import io.getlime.security.powerauth.biometry.IBiometricKeyEncryptor; import io.getlime.security.powerauth.biometry.IBiometricKeystore; +import io.getlime.security.powerauth.core.CryptoUtils; import io.getlime.security.powerauth.system.PowerAuthLog; /** @@ -41,7 +45,8 @@ @RequiresApi(api = Build.VERSION_CODES.M) public class BiometricKeystore implements IBiometricKeystore { - private static final String KEY_NAME = "io.getlime.PowerAuthKeychain.KeyStore.BiometryKeychain"; + private static final String KEY_NAME_PREFIX = "com.wultra.powerauth.biometricKey."; + private static final String LEGACY_KEY_NAME = "io.getlime.PowerAuthKeychain.KeyStore.BiometryKeychain"; private static final String PROVIDER_NAME = "AndroidKeyStore"; private KeyStore mKeyStore; @@ -56,61 +61,40 @@ public BiometricKeystore() { } } - /** - * Check if the Keystore is ready. - * @return True if Keystore is ready, false otherwise. - */ @Override public boolean isKeystoreReady() { return mKeyStore != null; } - /** - * Check if a default key is present in Keystore - * - * @return True in case a default key is present, false otherwise. Method returns false in case Keystore is not properly initialized (call {@link #isKeystoreReady()}). - */ @Override - public boolean containsBiometricKeyEncryptor() { + public boolean containsBiometricKeyEncryptor(@NonNull String keyId) { if (!isKeystoreReady()) { return false; } try { - return mKeyStore.containsAlias(KEY_NAME); + return mKeyStore.containsAlias(getKeystoreAlias(keyId)); } catch (KeyStoreException e) { PowerAuthLog.e("BiometricKeystore.containsBiometricKeyEncryptor failed: " + e.getMessage()); return false; } } - /** - * Generate a new biometry related Keystore key with default key name. - * - * The key that is created during this process is used to encrypt key stored in shared preferences, - * in order to derive key used for biometric authentication. - * @param invalidateByBiometricEnrollment If true, then internal key stored in KeyStore will be invalidated on next biometric enrollment. - * @param useSymmetricKey If true, then symmetric key will be created. - * @return New generated {@link SecretKey} key or {@code null} in case of failure. - */ @Override public @Nullable - IBiometricKeyEncryptor createBiometricKeyEncryptor(boolean invalidateByBiometricEnrollment, boolean useSymmetricKey) { - removeBiometricKeyEncryptor(); + IBiometricKeyEncryptor createBiometricKeyEncryptor(@NonNull String keyId, boolean invalidateByBiometricEnrollment, boolean useSymmetricKey) { + removeBiometricKeyEncryptor(keyId); if (useSymmetricKey) { - return BiometricKeyEncryptorAes.createAesEncryptor(PROVIDER_NAME, KEY_NAME, invalidateByBiometricEnrollment); + return BiometricKeyEncryptorAes.createAesEncryptor(PROVIDER_NAME, getKeystoreAlias(keyId), invalidateByBiometricEnrollment); } else { - return BiometricKeyEncryptorRsa.createRsaEncryptor(PROVIDER_NAME, KEY_NAME, invalidateByBiometricEnrollment); + return BiometricKeyEncryptorRsa.createRsaEncryptor(PROVIDER_NAME, getKeystoreAlias(keyId), invalidateByBiometricEnrollment); } } - /** - * Removes an encryption key from Keystore. - */ @Override - public void removeBiometricKeyEncryptor() { + public void removeBiometricKeyEncryptor(@NonNull String keyId) { try { - if (containsBiometricKeyEncryptor()) { - mKeyStore.deleteEntry(KEY_NAME); + if (containsBiometricKeyEncryptor(keyId)) { + mKeyStore.deleteEntry(getKeystoreAlias(keyId)); } } catch (KeyStoreException e) { PowerAuthLog.e("BiometricKeystore.removeBiometricKeyEncryptor failed: " + e.getMessage()); @@ -122,13 +106,13 @@ public void removeBiometricKeyEncryptor() { */ @Override @Nullable - public IBiometricKeyEncryptor getBiometricKeyEncryptor() { + public IBiometricKeyEncryptor getBiometricKeyEncryptor(@NonNull String keyId) { if (!isKeystoreReady()) { return null; } try { mKeyStore.load(null); - final Key key = mKeyStore.getKey(KEY_NAME, null); + final Key key = mKeyStore.getKey(getKeystoreAlias(keyId), null); if (key instanceof SecretKey) { // AES symmetric key return new BiometricKeyEncryptorAes((SecretKey)key); @@ -145,4 +129,27 @@ public IBiometricKeyEncryptor getBiometricKeyEncryptor() { } } + @Override + @NonNull + public String getLegacySharedKeyId() { + return LEGACY_KEY_NAME; + } + + /** + * Function return alias for key stored in KeyStore for given key identifier. If the key identifier is equal to + * legacy key name, then the alias is legacy key name. Otherwise, the key alias is calculated as + * {@code KEY_NAME_PREFIX + SHA256(keyId)}. + * @param keyId Key identifier. + * @return Key alias to key stored in KeyStore. + */ + @NonNull + private String getKeystoreAlias(@NonNull String keyId) { + if (LEGACY_KEY_NAME.equals(keyId)) { + return LEGACY_KEY_NAME; + } + final String keyIdHash = Base64.encodeToString( + CryptoUtils.hashSha256(keyId.getBytes(StandardCharsets.UTF_8)), + Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + return KEY_NAME_PREFIX + keyIdHash; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/DefaultBiometricKeyEncryptorProvider.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/DefaultBiometricKeyEncryptorProvider.java index 74b10f42..5f2ed976 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/DefaultBiometricKeyEncryptorProvider.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/DefaultBiometricKeyEncryptorProvider.java @@ -54,12 +54,12 @@ public boolean isAuthenticationRequiredOnEncryption() { public IBiometricKeyEncryptor getBiometricKeyEncryptor() throws PowerAuthErrorException { if (encryptor == null) { if (request.isForceGenerateNewKey()) { - encryptor = keystore.createBiometricKeyEncryptor(request.isInvalidateByBiometricEnrollment(), request.isUseSymmetricCipher()); + encryptor = keystore.createBiometricKeyEncryptor(request.getKeystoreAlias(), request.isInvalidateByBiometricEnrollment(), request.isUseSymmetricCipher()); if (encryptor == null) { throw new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, "Keystore failed to generate a new biometric key."); } } else { - encryptor = keystore.getBiometricKeyEncryptor(); + encryptor = keystore.getBiometricKeyEncryptor(request.getKeystoreAlias()); if (encryptor == null) { throw new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, "Cannot get biometric key from the keystore."); } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/dummy/DummyBiometricKeystore.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/dummy/DummyBiometricKeystore.java index eea239c4..6a10b014 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/dummy/DummyBiometricKeystore.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/dummy/DummyBiometricKeystore.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.biometry.impl.dummy; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.getlime.security.powerauth.biometry.IBiometricKeyEncryptor; @@ -33,23 +34,29 @@ public boolean isKeystoreReady() { } @Override - public boolean containsBiometricKeyEncryptor() { + public boolean containsBiometricKeyEncryptor(@NonNull String keyId) { return false; } @Nullable @Override - public IBiometricKeyEncryptor createBiometricKeyEncryptor(boolean invalidateByBiometricEnrollment, boolean useSymmetricKey) { + public IBiometricKeyEncryptor createBiometricKeyEncryptor(@NonNull String keyId, boolean invalidateByBiometricEnrollment, boolean useSymmetricKey) { return null; } @Override - public void removeBiometricKeyEncryptor() { + public void removeBiometricKeyEncryptor(@NonNull String keyId) { } @Nullable @Override - public IBiometricKeyEncryptor getBiometricKeyEncryptor() { + public IBiometricKeyEncryptor getBiometricKeyEncryptor(@NonNull String keyId) { return null; } + + @NonNull + @Override + public String getLegacySharedKeyId() { + return ""; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfiguration.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfiguration.java index 31d9272f..ffd5239d 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfiguration.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthKeychainConfiguration.java @@ -18,6 +18,7 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.getlime.security.powerauth.keychain.KeychainProtection; /** @@ -28,19 +29,22 @@ public class PowerAuthKeychainConfiguration { public static final String KEYCHAIN_ID_STATUS = "io.getlime.PowerAuthKeychain.StatusKeychain"; public static final String KEYCHAIN_ID_BIOMETRY = "io.getlime.PowerAuthKeychain.BiometryKeychain"; public static final String KEYCHAIN_ID_TOKEN_STORE = "io.getlime.PowerAuthKeychain.TokenStoreKeychain"; - public static final String KEYCHAIN_KEY_BIOMETRY_DEFAULT = "io.getlime.PowerAuthKeychain.BiometryKeychain.DefaultKey"; + public static final String KEYCHAIN_KEY_BIOMETRY_DEFAULT = null; + public static final String KEYCHAIN_KEY_SHARED_BIOMETRY_KEY = "io.getlime.PowerAuthKeychain.BiometryKeychain.DefaultKey"; public static final boolean DEFAULT_LINK_BIOMETRY_ITEMS_TO_CURRENT_SET = true; public static final boolean DEFAULT_CONFIRM_BIOMETRIC_AUTHENTICATION = false; public static final boolean DEFAULT_AUTHENTICATE_ON_BIOMETRIC_KEY_SETUP = true; + public static final boolean DEFAULT_ENABLE_FALLBACK_TO_SHARED_BIOMETRY_KEY = true; public static final @KeychainProtection int DEFAULT_REQUIRED_KEYCHAIN_PROTECTION = KeychainProtection.NONE; private final @NonNull String keychainIdStatus; private final @NonNull String keychainIdBiometry; private final @NonNull String keychainIdTokenStore; - private final @NonNull String keychainKeyBiometryDefault; + private final @Nullable String keychainKeyBiometry; private final boolean linkBiometricItemsToCurrentSet; private final boolean confirmBiometricAuthentication; private final boolean authenticateOnBiometricKeySetup; + private final boolean enableFallbackToSharedBiometryKey; private final @KeychainProtection int minimalRequiredKeychainProtection; /** @@ -61,10 +65,21 @@ public class PowerAuthKeychainConfiguration { /** * Get name of the Keychain key used for storing the default biometry key information. - * @return Name of the biometry Keychain key. + * @return Name of the default biometry Keychain key. + * @deprecated Use {@link #getKeychainKeyBiometry()} method instead. */ + @Deprecated // 1.7.10 - remove in 1.10.0 public @NonNull String getKeychainBiometryDefaultKey() { - return keychainKeyBiometryDefault; + return keychainKeyBiometry == null ? KEYCHAIN_KEY_SHARED_BIOMETRY_KEY : keychainKeyBiometry; + } + + /** + * Get name of the Keychain key used for storing the biometry key information for the PowerAuthSDK instance. If null + * then PowerAuthSDK instance will use its instance identifier to store the biometry key information. + * @return Get name of the Keychain key used for storing the biometry key information for the PowerAuthSDK instance. + */ + public @Nullable String getKeychainKeyBiometry() { + return keychainKeyBiometry; } /** @@ -110,6 +125,15 @@ public boolean isAuthenticateOnBiometricKeySetup() { return authenticateOnBiometricKeySetup; } + /** + * Get whether fallback to shared, legacy biometry key is enabled. By default, this is enabled for the compatibility + * reasons. If + * @return {@code true} if fallback to shared, legacy biometry key is enabled. + */ + public boolean isFallbackToSharedBiometryKeyEnabled() { + return enableFallbackToSharedBiometryKey; + } + /** * Get minimal required keychain protection level that must be supported on the current device. * If the level of protection on the device is insufficient, then you cannot use PowerAuth @@ -128,7 +152,7 @@ public boolean isAuthenticateOnBiometricKeySetup() { * * @param keychainIdStatus Name of the Keychain file used for storing the status information. * @param keychainIdBiometry Name of the Keychain file used for storing the biometry key information. - * @param keychainKeyBiometryDefault Name of the Keychain key used to store the default biometry key. + * @param keychainKeyBiometry Name of the Keychain key used for storing the biometry key information for the PowerAuthSDK instance. * @param keychainIdTokenStore Name of the Keychain file used for storing the access tokens. * @param linkBiometricItemsToCurrentSet If set, then the item protected with the biometry is invalidated * if fingers are added or removed, or if the user re-enrolls for face. @@ -137,25 +161,29 @@ public boolean isAuthenticateOnBiometricKeySetup() { * and may be ignored. * @param authenticateOnBiometricKeySetup If set, then the biometric key setup always require biometric authentication. * If not set, then only usage of biometric key require biometric authentication. + * @param enableFallbackToSharedBiometryKey If set, then the PowerAuthSDK does one more additional lookup to use legacy + * key shared between multiple PowerAuthSDK instances. * @param minimalRequiredKeychainProtection {@link KeychainProtection} constant with minimal required keychain * protection level that must be supported on the current device. */ private PowerAuthKeychainConfiguration( @NonNull String keychainIdStatus, @NonNull String keychainIdBiometry, - @NonNull String keychainKeyBiometryDefault, + @Nullable String keychainKeyBiometry, @NonNull String keychainIdTokenStore, boolean linkBiometricItemsToCurrentSet, boolean confirmBiometricAuthentication, boolean authenticateOnBiometricKeySetup, + boolean enableFallbackToSharedBiometryKey, @KeychainProtection int minimalRequiredKeychainProtection) { this.keychainIdStatus = keychainIdStatus; this.keychainIdBiometry = keychainIdBiometry; - this.keychainKeyBiometryDefault = keychainKeyBiometryDefault; + this.keychainKeyBiometry = keychainKeyBiometry; this.keychainIdTokenStore = keychainIdTokenStore; this.linkBiometricItemsToCurrentSet = linkBiometricItemsToCurrentSet; this.confirmBiometricAuthentication = confirmBiometricAuthentication; this.authenticateOnBiometricKeySetup = authenticateOnBiometricKeySetup; + this.enableFallbackToSharedBiometryKey = enableFallbackToSharedBiometryKey; this.minimalRequiredKeychainProtection = minimalRequiredKeychainProtection; } @@ -167,10 +195,11 @@ public static class Builder { private @NonNull String keychainStatusId = KEYCHAIN_ID_STATUS; private @NonNull String keychainBiometryId = KEYCHAIN_ID_BIOMETRY; private @NonNull String keychainTokenStoreId = KEYCHAIN_ID_TOKEN_STORE; - private @NonNull String keychainBiometryDefaultKey = KEYCHAIN_KEY_BIOMETRY_DEFAULT; + private @Nullable String keychainKeyBiometry = KEYCHAIN_KEY_BIOMETRY_DEFAULT; private boolean linkBiometricItemsToCurrentSet = DEFAULT_LINK_BIOMETRY_ITEMS_TO_CURRENT_SET; private boolean confirmBiometricAuthentication = DEFAULT_CONFIRM_BIOMETRIC_AUTHENTICATION; private boolean authenticateOnBiometricKeySetup = DEFAULT_AUTHENTICATE_ON_BIOMETRIC_KEY_SETUP; + private boolean enableFallbackToSharedBiometryKey = DEFAULT_ENABLE_FALLBACK_TO_SHARED_BIOMETRY_KEY; private @KeychainProtection int minimalRequiredKeychainProtection = DEFAULT_REQUIRED_KEYCHAIN_PROTECTION; /** @@ -215,11 +244,23 @@ public Builder() { /** * Set name of the Keychain key used to store the default biometry key. * - * @param keychainBiometryDefaultKey Name of the Keychain key used to store the default biometry key. + * @param keychainKeyBiometry Name of the Keychain key used to store the default biometry key. + * @return {@link Builder} + * @deprecated Use {@link #keychainKeyBiometry(String)} as a replacement. + */ + @Deprecated // 1.7.10 - remove in 1.10.0 + public @NonNull Builder keychainBiometryDefaultKey(@NonNull String keychainKeyBiometry) { + this.keychainKeyBiometry = keychainKeyBiometry; + return this; + } + + /** + * Set the name of the key to the biometry Keychain to store biometry-factor protection key. + * @param keychainKeyBiometry name of the key to biometry keychain to store data containing biometry related encryption key. * @return {@link Builder} */ - public @NonNull Builder keychainBiometryDefaultKey(@NonNull String keychainBiometryDefaultKey) { - this.keychainBiometryDefaultKey = keychainBiometryDefaultKey; + public @NonNull Builder keychainKeyBiometry(@NonNull String keychainKeyBiometry) { + this.keychainKeyBiometry = keychainKeyBiometry; return this; } @@ -257,7 +298,7 @@ public Builder() { *
* If set to {@code false}, then RSA cipher is used and only the usage of biometric key * require the biometric authentication. This is due to fact, that RSA cipher can encrypt - * data with using it's public key available immediate after the key-pair is created in + * data with using its public key available immediate after the key-pair is created in * Android KeyStore. *
* The default value is {@code true}. @@ -271,6 +312,20 @@ public Builder() { return this; } + /** + * (Optional) Set, whether PowerAuthSDK instance should also do additional lookup for a legacy biometric key, + * previously shared between multiple PowerAuthSDK object instances. + *
+ * The default value is {@code true} and the fallback is enabled. + * + * @param enable If {@code true} then fallback to legacy key is enabled. + * @return {@link Builder} + */ + public @NonNull Builder enableFallbackToSharedBiometryKey(boolean enable) { + this.enableFallbackToSharedBiometryKey = enable; + return this; + } + /** * Set minimal required keychain protection level that must be supported on the current device. Note that * if you enforce protection higher that {@link KeychainProtection#NONE}, then your application must target @@ -294,11 +349,12 @@ public Builder() { return new PowerAuthKeychainConfiguration( keychainStatusId, keychainBiometryId, - keychainBiometryDefaultKey, + keychainKeyBiometry, keychainTokenStoreId, linkBiometricItemsToCurrentSet, confirmBiometricAuthentication, authenticateOnBiometricKeySetup, + enableFallbackToSharedBiometryKey, minimalRequiredKeychainProtection); } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index a42db602..9744fe9a 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -78,6 +78,7 @@ public class PowerAuthSDK { private final @NonNull IServerStatusProvider mServerStatusProvider; private final @NonNull TimeSynchronizationService mTimeSynchronizationService; private final @NonNull IKeystoreService mKeystoreService; + private final @NonNull BiometricDataMapper mBiometricDataMapper; /** * A builder that collects configurations and arguments for {@link PowerAuthSDK}. @@ -214,6 +215,9 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti // Prepare low-level Session object. final Session session = new Session(mConfiguration.getSessionSetup(), timeSynchronizationService); + // Prepare biometric data mapping provider + final BiometricDataMapper biometricDataMapper = new BiometricDataMapper(sharedLock, session, mConfiguration, mKeychainConfiguration, biometryKeychain); + // Prepare keystore service and conned it with HTTP client final DefaultKeystoreService keystoreService = new DefaultKeystoreService(timeSynchronizationService, session, mCallbackDispatcher, sharedLock, httpClient); httpClient.setKeystoreService(keystoreService); @@ -230,6 +234,7 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti possessionEncryptionKeyProvider, biometryKeychain, tokenStoreKeychain, + biometricDataMapper, mCallbackDispatcher, serverStatusProvider, timeSynchronizationService, @@ -272,6 +277,7 @@ private PowerAuthSDK( @NonNull IPossessionFactorEncryptionKeyProvider possessionKeyProvider, @NonNull Keychain biometryKeychain, @NonNull Keychain tokenStoreKeychain, + @NonNull BiometricDataMapper biometricDataMapper, @NonNull ICallbackDispatcher callbackDispatcher, @NonNull IServerStatusProvider serverStatusProvider, @NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService, @@ -285,6 +291,7 @@ private PowerAuthSDK( this.mStateListener = stateListener; this.mPossessionFactorEncryptionKeyProvider = possessionKeyProvider; this.mBiometryKeychain = biometryKeychain; + this.mBiometricDataMapper = biometricDataMapper; this.mCallbackDispatcher = callbackDispatcher; this.mTokenStore = new PowerAuthTokenStore(this, tokenStoreKeychain, client); this.mServerStatusProvider = serverStatusProvider; @@ -1596,40 +1603,21 @@ public void onCancel() { * user has to remove the activation by using another channel (typically internet banking, or similar web management console) *
* WARNING: Note that if you have multiple activated SDK instances used in your application at the same time, then you should keep - * shared biometry key intact if it's still used in another SDK instance. For this kind of situations, it's recommended to use + * shared biometry key intact if it's still used in another SDK instance. For this kind of situation, it's recommended to use * another form of this method, where you can decide whether the key should be removed. * * @param context Context * @throws PowerAuthMissingConfigException thrown in case configuration is not present. */ public void removeActivationLocal(@NonNull Context context) { - removeActivationLocal(context, true); - } - - /** - * Removes existing activation from the device. - *
- * This method removes the activation session state and optionally also shared biometry factor key. Cached possession related - * key remains intact. Unlike the `removeActivationWithAuthentication`, this method doesn't inform server about activation removal. - * In this case user has to remove the activation by using another channel (typically internet banking, or similar web management console) - *
- * NOTE: This method is useful for situations, where the application has multiple SDK instances activated at the same time and - * you need to manage a lifetime of shared biometry key. - * - * @param context Android context. - * @param removeSharedBiometryKey If set to true, then also shared biometry key will be removed. - * @throws PowerAuthMissingConfigException thrown in case configuration is not present. - */ - public void removeActivationLocal(@NonNull Context context, boolean removeSharedBiometryKey) { - checkForValidSetup(); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (removeSharedBiometryKey && mSession.hasBiometryFactor()) { - mBiometryKeychain.remove(mKeychainConfiguration.getKeychainBiometryDefaultKey()); - } - BiometricAuthentication.getBiometricKeystore().removeBiometricKeyEncryptor(); + final BiometricDataMapper.Mapping biometricDataMapping = mBiometricDataMapper.getMapping(null, context, BiometricDataMapper.BIO_MAPPING_REMOVE_KEY); + if (mSession.hasBiometryFactor()) { + mBiometryKeychain.remove(biometricDataMapping.keychainKey); } + BiometricAuthentication.getBiometricKeystore().removeBiometricKeyEncryptor(biometricDataMapping.keystoreId); + // Remove all tokens from token store getTokenStore().cancelAllRequests(); getTokenStore().removeAllLocalTokens(context); @@ -1644,6 +1632,26 @@ public void removeActivationLocal(@NonNull Context context, boolean removeShared clearCachedData(); } + /** + * Removes existing activation from the device. + *
+ * This method removes the activation session state and optionally also shared biometry factor key. Cached possession related + * key remains intact. Unlike the `removeActivationWithAuthentication`, this method doesn't inform server about activation removal. + * In this case user has to remove the activation by using another channel (typically internet banking, or similar web management console) + *
+ * NOTE:The removeSharedBiometryKey parameter is now ignored, because PowerAuthSDK no longer use the shared key for a newly created + * biometry factors. + * + * @param context Android context. + * @param removeSharedBiometryKey This parameter is ignored. + * @throws PowerAuthMissingConfigException thrown in case configuration is not present. + * @deprecated Use {@link #removeActivationLocal(Context)} as a replacement. + */ + @Deprecated // 1.7.10 - remove in 1.10.0 + public void removeActivationLocal(@NonNull Context context, boolean removeSharedBiometryKey) { + removeActivationLocal(context); + } + /** * Clear in-memory cached data. */ @@ -1950,10 +1958,11 @@ public boolean hasBiometryFactor(@NonNull Context context) { // Initialize keystore final IBiometricKeystore keyStore = BiometricAuthentication.getBiometricKeystore(); + final BiometricDataMapper.Mapping biometricDataMapping = mBiometricDataMapper.getMapping(keyStore, context, BiometricDataMapper.BIO_MAPPING_NOOP); // Check if there is biometry factor in session, key in PA2Keychain and key in keystore. - return mSession.hasBiometryFactor() && keyStore.containsBiometricKeyEncryptor() && - mBiometryKeychain.contains(mKeychainConfiguration.getKeychainBiometryDefaultKey()); + return mSession.hasBiometryFactor() && keyStore.containsBiometricKeyEncryptor(biometricDataMapping.keystoreId) && + mBiometryKeychain.contains(biometricDataMapping.keychainKey); } /** @@ -2226,9 +2235,11 @@ public boolean removeBiometryFactor(@NonNull Context context) { final int result = mSession.removeBiometryFactor(); if (result == ErrorCode.OK) { // Update state after each successful calculations + final IBiometricKeystore keystore = BiometricAuthentication.getBiometricKeystore(); + final BiometricDataMapper.Mapping biometricDataMapping = mBiometricDataMapper.getMapping(keystore, context, BiometricDataMapper.BIO_MAPPING_REMOVE_KEY); saveSerializedState(); - mBiometryKeychain.remove(mKeychainConfiguration.getKeychainBiometryDefaultKey()); - BiometricAuthentication.getBiometricKeystore().removeBiometricKeyEncryptor(); + mBiometryKeychain.remove(biometricDataMapping.keychainKey); + keystore.removeBiometricKeyEncryptor(biometricDataMapping.keystoreId); } return result == ErrorCode.OK; } @@ -2464,13 +2475,14 @@ private ICancelable authenticateUsingBiometrics( final boolean forceGenerateNewKey, final @NonNull IBiometricAuthenticationCallback callback) { + final BiometricDataMapper.Mapping biometricDataMapping = mBiometricDataMapper.getMapping(null, context, forceGenerateNewKey ? BiometricDataMapper.BIO_MAPPING_CREATE_KEY : BiometricDataMapper.BIO_MAPPING_NOOP); final byte[] rawKeyData; if (forceGenerateNewKey) { // new key has to be generated rawKeyData = mSession.generateSignatureUnlockKey(); } else { // old key should be used, if present - rawKeyData = mBiometryKeychain.getData(mKeychainConfiguration.getKeychainBiometryDefaultKey()); + rawKeyData = mBiometryKeychain.getData(biometricDataMapping.keychainKey); } if (rawKeyData == null) { @@ -2488,6 +2500,7 @@ private ICancelable authenticateUsingBiometrics( .setTitle(title) .setDescription(description) .setRawKeyData(rawKeyData) + .setKeystoreAlias(biometricDataMapping.keystoreId) .setForceGenerateNewKey(forceGenerateNewKey, mKeychainConfiguration.isLinkBiometricItemsToCurrentSet(), mKeychainConfiguration.isAuthenticateOnBiometricKeySetup()) .setUserConfirmationRequired(mKeychainConfiguration.isConfirmBiometricAuthentication()) .setBackgroundTaskExecutor(mExecutorProvider.getConcurrentExecutor()); @@ -2508,7 +2521,7 @@ public void onBiometricDialogCancelled(boolean userCancel) { public void onBiometricDialogSuccess(@NonNull BiometricKeyData biometricKeyData) { // Store the new key, if a new key was generated if (biometricKeyData.isNewKey()) { - mBiometryKeychain.putData(biometricKeyData.getDataToSave(), mKeychainConfiguration.getKeychainBiometryDefaultKey()); + mBiometryKeychain.putData(biometricKeyData.getDataToSave(), biometricDataMapping.keychainKey); } byte[] normalizedEncryptionKey = mSession.normalizeSignatureUnlockKeyFromData(biometricKeyData.getDerivedData()); callback.onBiometricDialogSuccess(new BiometricKeyData(biometricKeyData.getDataToSave(), normalizedEncryptionKey, biometricKeyData.isNewKey())); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/BiometricDataMapper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/BiometricDataMapper.java new file mode 100644 index 00000000..5891d009 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/BiometricDataMapper.java @@ -0,0 +1,186 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.biometry.BiometricAuthentication; +import io.getlime.security.powerauth.biometry.IBiometricKeystore; +import io.getlime.security.powerauth.core.Session; +import io.getlime.security.powerauth.keychain.Keychain; +import io.getlime.security.powerauth.sdk.PowerAuthConfiguration; +import io.getlime.security.powerauth.sdk.PowerAuthKeychainConfiguration; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * The {@code BiometricDataMapping} is a helper class that provides mapping for biometry-related encryption keys stored + * in the {@link Keychain} and the {@code Android KeyStore}. Previous versions of the SDK used a shared key for all + * {@code PowerAuthSDK} instances. This behavior was changed in the following SDK versions to use unique keys generated + * for each instance: + *
+ * Related issue: Biometrics not working on multiple + * instances. + */ +public class BiometricDataMapper { + + /** + * The {@code Mapping} class contains information where the biometry-related encryption key is stored. + */ + public static class Mapping { + /** + * If {@code true}, then this mapping points to the shared key. + */ + public final boolean isSharedKey; + /** + * Key identifier (or alias) to the Android KeyStore. + */ + public final @NonNull String keystoreId; + /** + * Storage key to the PowerAuth {@link Keychain}. + */ + public final @NonNull String keychainKey; + + /** + * Construct mapping with required parameters. + * @param isSharedKey Information whether the key is shared. + * @param keystoreId Key identifier (or alias) to the Android KeyStore. + * @param keychainKey Storage key to the PowerAuth {@link Keychain}. + */ + Mapping(boolean isSharedKey, @NonNull String keystoreId, @NonNull String keychainKey) { + this.isSharedKey = isSharedKey; + this.keystoreId = keystoreId; + this.keychainKey = keychainKey; + } + } + + /** + * No additional operation is required when the mapping is created. + */ + public static final int BIO_MAPPING_NOOP = 0; + /** + * The mapping is required when the biometry-related encryption key is being newly crated. In this case, the mapper + * will always return a mapping to the per-instance data. + */ + public static final int BIO_MAPPING_CREATE_KEY = 2; + /** + * The mapping is required when the biometry-related encryption key is being removed. If the current mapping contains + * the mapping to the shared, legacy key, then the mapper will return this legacy mapping. The next call to + * {@link #getMapping(IBiometricKeystore, Context, int)} will provide a mapping to the per-instance data. + */ + public static final int BIO_MAPPING_REMOVE_KEY = 2; + + private final ReentrantLock lock; + private final Session session; + private final String instanceId; + private final String keychainStorageKey; + private final boolean isFallbackToSharedBiometryKeyEnabled; + private final Keychain biometricKeychain; + + /** + * The current mapping. + */ + private Mapping mapping; + + /** + * Create a helper object with all required parameters. + * @param sharedLock Instance of lock shared between multiple internal SDK objects. + * @param session Session instance. + * @param configuration PowerAuth SDK instance configuration. + * @param keychainConfiguration PowerAuth SDK keychain configuration. + * @param biometricKeychain A Keychain for storing biometry-related encryption keys. + */ + public BiometricDataMapper( + @NonNull ReentrantLock sharedLock, + @NonNull Session session, + @NonNull PowerAuthConfiguration configuration, + @NonNull PowerAuthKeychainConfiguration keychainConfiguration, + @NonNull Keychain biometricKeychain) { + this.lock = sharedLock; + this.session = session; + this.instanceId = configuration.getInstanceId(); + this.keychainStorageKey = keychainConfiguration.getKeychainKeyBiometry(); + this.isFallbackToSharedBiometryKeyEnabled = keychainConfiguration.isFallbackToSharedBiometryKeyEnabled(); + this.biometricKeychain = biometricKeychain; + } + + /** + * Get the mapping for stored biometry-related encryption keys. + * @param keyStore Instance of {@link IBiometricKeystore} object. If not provided, then function gets default, shared keystore. + * @param context Android context object. + * @param purpose Specify situation in which the mapping is acquired. Use {@code BIO_MAPPING_*} constants from this class. + * @return Mapping for stored biometry-related encryption keys. + */ + @NonNull + public Mapping getMapping(@Nullable IBiometricKeystore keyStore, @NonNull Context context, int purpose) { + try { + lock.lock(); + + if (keyStore == null) { + keyStore = BiometricAuthentication.getBiometricKeystore(); + } + + // New per-instance identifiers + final String instanceKeystoreId = instanceId; + final String instanceKeychainKey = keychainStorageKey != null ? keychainStorageKey : instanceId; + + if (mapping == null) { + if ((purpose != BIO_MAPPING_CREATE_KEY) && isFallbackToSharedBiometryKeyEnabled) { + // Legacy identifiers. + final String legacyKeystoreId = keyStore.getLegacySharedKeyId(); + final String legacyKeychainKey = keychainStorageKey != null ? keychainStorageKey : PowerAuthKeychainConfiguration.KEYCHAIN_KEY_SHARED_BIOMETRY_KEY; + if (session.hasBiometryFactor()) { + // Looks like session has a biometry factor configured. + // if per-instance keys are set, then don't use the shared encryptor and keychain data. We already have a new + // setup applied on per-instance basis. + if (!biometricKeychain.contains(instanceKeychainKey) && !keyStore.containsBiometricKeyEncryptor(instanceKeystoreId)) { + if (biometricKeychain.contains(legacyKeychainKey) && keyStore.containsBiometricKeyEncryptor(legacyKeystoreId)) { + // Looks like keychain and keystore contains data for a shared key, so try to use such key instead. + mapping = new Mapping(true, legacyKeystoreId, legacyKeychainKey); + } + } + } + } + if (mapping == null) { + // Legacy config was not created, so create a new one, to use per-instance identifiers. + mapping = new Mapping(false, instanceKeystoreId, instanceKeychainKey); + } + } + if (purpose == BIO_MAPPING_REMOVE_KEY) { + // We're going to remove key. If the current config is still legacy, then we should return this legacy + // mapping and simultaneously setup a new one. + if (mapping.isSharedKey) { + final Mapping legacyMapping = mapping; + mapping = new Mapping(false, instanceKeystoreId, instanceKeychainKey); + return legacyMapping; + } + } + return mapping; + } finally { + lock.unlock(); + } + } +}