From 43f9358c07efd0d8df6b7b1e473bf4320b8c70c0 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 27 Feb 2026 04:21:04 +0530 Subject: [PATCH 1/5] =?UTF-8?q?SDK-7858=20fix:=20Handle=20ProviderExceptio?= =?UTF-8?q?n=20in=20PKCS1=E2=86=92OAEP=20key=20migration=20to=20prevent=20?= =?UTF-8?q?saveCredentials()=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authentication/storage/CryptoUtil.java | 25 +++- .../storage/CryptoUtilTest.java | 141 ++++++++++++++++++ 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java index e0b175e88..ef31c448f 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java @@ -343,7 +343,8 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_SPEC); return cipher.doFinal(encryptedInput); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) { + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | ProviderException e) { /* * This exceptions are safe to be ignored: * @@ -356,6 +357,11 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry * Thrown if the given key is inappropriate for initializing this cipher. * - InvalidAlgorithmParameterException: * Thrown if the OAEP parameters are invalid or unsupported. + * - ProviderException: + * Thrown on Android 12+ (Keystore2) when the key's padding restriction is + * incompatible with the cipher transformation (e.g. a PKCS1-restricted key + * initialised with an OAEP spec). On Android < 12 this surfaces as + * InvalidKeyException instead. * * Read more in https://developer.android.com/reference/javax/crypto/Cipher */ @@ -394,7 +400,8 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey(), OAEP_SPEC); return cipher.doFinal(decryptedInput); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) { + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | ProviderException e) { /* * This exceptions are safe to be ignored: * @@ -407,6 +414,11 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry * Thrown if the given key is inappropriate for initializing this cipher. * - InvalidAlgorithmParameterException: * Thrown if the OAEP parameters are invalid or unsupported. + * - ProviderException: + * Thrown on Android 12+ (Keystore2) when the key's padding restriction is + * incompatible with the cipher transformation (e.g. a PKCS1-restricted key + * initialised with an OAEP spec). On Android < 12 this surfaces as + * InvalidKeyException instead. * * Read more in https://developer.android.com/reference/javax/crypto/Cipher */ @@ -593,7 +605,9 @@ private byte[] tryMigrateLegacyAESKey() { KeyStore.PrivateKeyEntry rsaKeyEntry = getRSAKeyEntry(); byte[] decryptedAESKey = RSADecryptLegacyPKCS1(encryptedOldAESBytes, rsaKeyEntry.getPrivateKey()); - + + deleteRSAKeys(); + // Re-encrypt with OAEP and store at new location byte[] encryptedAESWithOAEP = RSAEncrypt(decryptedAESKey); String newEncodedEncryptedAES = new String(Base64.encode(encryptedAESWithOAEP, Base64.DEFAULT), StandardCharsets.UTF_8); @@ -632,8 +646,11 @@ private byte[] generateNewAESKey() throws IncompatibleDeviceException, CryptoExc } catch (NoSuchAlgorithmException e) { Log.e(TAG, "AES algorithm not available.", e); throw new IncompatibleDeviceException(e); + } catch (IncompatibleDeviceException e) { + deleteRSAKeys(); + deleteAESKeys(); + throw e; } catch (CryptoException e) { - // Re-throw CryptoException and its subclasses (including IncompatibleDeviceException) throw e; } catch (Exception e) { Log.e(TAG, "Unexpected error while creating new AES key.", e); diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java index d9f84c910..e26104b00 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java @@ -16,6 +16,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; @@ -1941,4 +1942,144 @@ public void shouldGenerateNewKeyWhenMigrationFails() throws Exception { Mockito.verify(storage, times(1)).remove(KEY_ALIAS); Mockito.verify(storage, times(1)).remove(OLD_KEY_ALIAS); } + + @Test + public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsIncompatibleDevice() { + Assert.assertThrows("The device is not compatible with the CryptoUtil class", + IncompatibleDeviceException.class, () -> { + PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); + doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); + PowerMockito.mockStatic(Cipher.class); + PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + doThrow(new ProviderException(new KeyStoreException("Incompatible padding mode"))) + .when(rsaOaepCipher).init(eq(Cipher.DECRYPT_MODE), eq(privateKey), + any(AlgorithmParameterSpec.class)); + + cryptoUtil.RSADecrypt(new byte[]{1, 2, 3}); + }); + } + + @Test + public void shouldWrapProviderExceptionFromCipherInitInRSAEncryptAsIncompatibleDevice() { + Assert.assertThrows("The device is not compatible with the CryptoUtil class", + IncompatibleDeviceException.class, () -> { + PublicKey publicKey = PowerMockito.mock(PublicKey.class); + Certificate certificate = PowerMockito.mock(Certificate.class); + doReturn(publicKey).when(certificate).getPublicKey(); + KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + doReturn(certificate).when(privateKeyEntry).getCertificate(); + doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); + PowerMockito.mockStatic(Cipher.class); + PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + doThrow(new ProviderException(new KeyStoreException("Incompatible padding mode"))) + .when(rsaOaepCipher).init(eq(Cipher.ENCRYPT_MODE), eq(publicKey), + any(AlgorithmParameterSpec.class)); + + cryptoUtil.RSAEncrypt(new byte[]{1, 2, 3}); + }); + } + + @Test + public void shouldTriggerPKCS1MigrationWhenRSADecryptThrowsProviderException() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + byte[] encryptedAESPKCS1 = new byte[]{10, 11, 12, 13}; + byte[] aesKeyBytes = new byte[32]; + Arrays.fill(aesKeyBytes, (byte) 0xAB); + byte[] reEncryptedOAEP = new byte[]{20, 21, 22, 23}; + String encodedPKCS1 = "pkcs1_encoded"; + String encodedOAEP = "oaep_encoded"; + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(encodedPKCS1); + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESPKCS1); + PowerMockito.when(Base64.encode(reEncryptedOAEP, Base64.DEFAULT)) + .thenReturn(encodedOAEP.getBytes(StandardCharsets.UTF_8)); + + doThrow(new IncompatibleDeviceException( + new ProviderException(new KeyStoreException("Incompatible padding mode")))) + .when(cryptoUtil).RSADecrypt(encryptedAESPKCS1); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + KeyStore.PrivateKeyEntry mockEntry = mock(KeyStore.PrivateKeyEntry.class); + when(mockEntry.getPrivateKey()).thenReturn(mock(PrivateKey.class)); + when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) + .thenReturn(mockEntry); + when(rsaPkcs1Cipher.doFinal(encryptedAESPKCS1)).thenReturn(aesKeyBytes); + doReturn(reEncryptedOAEP).when(cryptoUtil).RSAEncrypt(aesKeyBytes); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(aesKeyBytes)); + Mockito.verify(storage).store(KEY_ALIAS, encodedOAEP); + Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); + } + + @Test + public void shouldDeleteOldRSAKeyBeforeReEncryptingInTryMigrateLegacyAESKey() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + byte[] aesKeyBytes = new byte[32]; + Arrays.fill(aesKeyBytes, (byte) 0xCD); + byte[] encryptedOldAES = new byte[]{1, 2, 3, 4}; + byte[] encryptedNewAES = new byte[]{4, 5, 6}; + String encodedOldAES = "old_pkcs1_encoded"; + String encodedNewAES = "new_oaep_encoded"; + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(encodedOldAES); + + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedOldAES, Base64.DEFAULT)).thenReturn(encryptedOldAES); + PowerMockito.when(Base64.encode(encryptedNewAES, Base64.DEFAULT)) + .thenReturn(encodedNewAES.getBytes(StandardCharsets.UTF_8)); + + KeyStore.PrivateKeyEntry mockEntry = mock(KeyStore.PrivateKeyEntry.class); + PrivateKey mockPrivateKey = mock(PrivateKey.class); + when(mockEntry.getPrivateKey()).thenReturn(mockPrivateKey); + doReturn(mockEntry).when(cryptoUtil).getRSAKeyEntry(); + + when(rsaPkcs1Cipher.doFinal(encryptedOldAES)).thenReturn(aesKeyBytes); + + doReturn(encryptedNewAES).when(cryptoUtil).RSAEncrypt(aesKeyBytes); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(aesKeyBytes)); + Mockito.verify(storage).store(KEY_ALIAS, encodedNewAES); + Mockito.verify(storage).remove(OLD_KEY_ALIAS); + + + InOrder inOrder = Mockito.inOrder(keyStore, cryptoUtil); + inOrder.verify(keyStore).deleteEntry(KEY_ALIAS); + inOrder.verify(keyStore).deleteEntry(OLD_KEY_ALIAS); + inOrder.verify(cryptoUtil).RSAEncrypt(aesKeyBytes); + } + + @Test + public void shouldDeleteStaleRSAKeyAndRethrowOnIncompatibleDeviceExceptionDuringGenerateNewAESKey() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + + byte[] newAESKey = new byte[32]; + Arrays.fill(newAESKey, (byte) 0xEF); + SecretKey mockSecret = mock(SecretKey.class); + when(mockSecret.getEncoded()).thenReturn(newAESKey); + when(keyGenerator.generateKey()).thenReturn(mockSecret); + + doThrow(new IncompatibleDeviceException( + new ProviderException(new KeyStoreException("Incompatible padding mode")))) + .when(cryptoUtil).RSAEncrypt(newAESKey); + + Assert.assertThrows(IncompatibleDeviceException.class, () -> cryptoUtil.getAESKey()); + + Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); + Mockito.verify(keyStore).deleteEntry(OLD_KEY_ALIAS); + Mockito.verify(storage).remove(KEY_ALIAS); + Mockito.verify(storage).remove(OLD_KEY_ALIAS); + } } From 15eb856ee80a295f74c60e9173a6e0151aad5976 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 6 Mar 2026 10:28:07 +0530 Subject: [PATCH 2/5] fix: Wrap ProviderException as CryptoException to enable key recovery on Android 12+ --- .../authentication/storage/CryptoUtil.java | 47 ++++++++------ .../storage/CryptoUtilTest.java | 12 ++-- .../com/auth0/sample/DatabaseLoginFragment.kt | 64 +++++++++++++++++-- .../res/layout/fragment_database_login.xml | 18 +++++- 4 files changed, 111 insertions(+), 30 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java index ef31c448f..e6a5a0006 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java @@ -344,10 +344,8 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_SPEC); return cipher.doFinal(encryptedInput); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | ProviderException e) { + | InvalidAlgorithmParameterException e) { /* - * This exceptions are safe to be ignored: - * * - NoSuchPaddingException: * Thrown if PKCS1Padding is not available. Was introduced in API 1. * - NoSuchAlgorithmException: @@ -357,16 +355,22 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry * Thrown if the given key is inappropriate for initializing this cipher. * - InvalidAlgorithmParameterException: * Thrown if the OAEP parameters are invalid or unsupported. - * - ProviderException: - * Thrown on Android 12+ (Keystore2) when the key's padding restriction is - * incompatible with the cipher transformation (e.g. a PKCS1-restricted key - * initialised with an OAEP spec). On Android < 12 this surfaces as - * InvalidKeyException instead. * * Read more in https://developer.android.com/reference/javax/crypto/Cipher */ Log.e(TAG, "The device can't decrypt input using a RSA Key.", e); throw new IncompatibleDeviceException(e); + } catch (ProviderException e) { + /* + * On Android 12+ (Keystore2), a padding mismatch throws ProviderException + * instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1 + * key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException + * allows the caller to fall through to key regeneration. + */ + Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e); + deleteAESKeys(); + throw new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", e); } catch (IllegalArgumentException | IllegalBlockSizeException | BadPaddingException e) { /* * Any of this exceptions mean the encrypted input is somehow corrupted and cannot be recovered. @@ -401,10 +405,8 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey(), OAEP_SPEC); return cipher.doFinal(decryptedInput); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | ProviderException e) { + | InvalidAlgorithmParameterException e) { /* - * This exceptions are safe to be ignored: - * * - NoSuchPaddingException: * Thrown if PKCS1Padding is not available. Was introduced in API 1. * - NoSuchAlgorithmException: @@ -414,16 +416,22 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry * Thrown if the given key is inappropriate for initializing this cipher. * - InvalidAlgorithmParameterException: * Thrown if the OAEP parameters are invalid or unsupported. - * - ProviderException: - * Thrown on Android 12+ (Keystore2) when the key's padding restriction is - * incompatible with the cipher transformation (e.g. a PKCS1-restricted key - * initialised with an OAEP spec). On Android < 12 this surfaces as - * InvalidKeyException instead. * * Read more in https://developer.android.com/reference/javax/crypto/Cipher */ Log.e(TAG, "The device can't encrypt input using a RSA Key.", e); throw new IncompatibleDeviceException(e); + } catch (ProviderException e) { + /* + * On Android 12+ (Keystore2), a padding mismatch throws ProviderException + * instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1 + * key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException + * allows the caller to fall through to key regeneration. + */ + Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e); + deleteAESKeys(); + throw new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", e); } catch (IllegalBlockSizeException | BadPaddingException e) { /* * They really should not be thrown at all since padding is requested in the transformation. @@ -479,10 +487,12 @@ private byte[] attemptPKCS1Migration(byte[] encryptedAESBytes) { } catch (BadPaddingException | IllegalBlockSizeException e) { Log.e(TAG, "PKCS1 decryption failed. Data may be corrupted.", e); - } catch (KeyStoreException | CertificateException | IOException | + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableEntryException | NoSuchPaddingException | InvalidKeyException e) { Log.e(TAG, "Migration failed due to key access error.", e); + } catch (ProviderException e) { + Log.e(TAG, "PKCS1 migration failed: key padding incompatible (Android 12+ Keystore2).", e); } catch (CryptoException e) { Log.e(TAG, "Failed to re-encrypt AES key with OAEP.", e); } @@ -617,7 +627,8 @@ private byte[] tryMigrateLegacyAESKey() { Log.d(TAG, "Legacy AES key migrated successfully"); return decryptedAESKey; } catch (CryptoException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | - BadPaddingException | IllegalBlockSizeException | IllegalArgumentException e) { + BadPaddingException | IllegalBlockSizeException | IllegalArgumentException | + ProviderException e) { Log.e(TAG, "Could not migrate legacy AES key. Will generate new key.", e); deleteAESKeys(); return null; diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java index e26104b00..55b1f507c 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java @@ -1944,9 +1944,9 @@ public void shouldGenerateNewKeyWhenMigrationFails() throws Exception { } @Test - public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsIncompatibleDevice() { - Assert.assertThrows("The device is not compatible with the CryptoUtil class", - IncompatibleDeviceException.class, () -> { + public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsCryptoException() { + Assert.assertThrows("The RSA key's padding mode is incompatible with the current cipher.", + CryptoException.class, () -> { PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); @@ -1962,9 +1962,9 @@ public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsIncompatibleD } @Test - public void shouldWrapProviderExceptionFromCipherInitInRSAEncryptAsIncompatibleDevice() { - Assert.assertThrows("The device is not compatible with the CryptoUtil class", - IncompatibleDeviceException.class, () -> { + public void shouldWrapProviderExceptionFromCipherInitInRSAEncryptAsCryptoException() { + Assert.assertThrows("The RSA key's padding mode is incompatible with the current cipher.", + CryptoException.class, () -> { PublicKey publicKey = PowerMockito.mock(PublicKey.class); Certificate certificate = PowerMockito.mock(Certificate.class); doReturn(publicKey).when(certificate).getPublicKey(); diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 918de531b..81f01c7a3 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -2,6 +2,7 @@ package com.auth0.sample import android.os.Bundle import android.os.CancellationSignal +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -50,6 +51,11 @@ import java.util.concurrent.Executors */ class DatabaseLoginFragment : Fragment() { + companion object { + private const val TAG = "Auth0CrashRepro" + private const val SDK_VERSION = "3.13.0 (OAEP)" + } + private val scope = "openid profile email read:current_user update:current_user_metadata" private val account: Auth0 by lazy { @@ -122,6 +128,8 @@ class DatabaseLoginFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val binding = FragmentDatabaseLoginBinding.inflate(inflater, container, false) + binding.tvSdkVersion.text = "SDK: $SDK_VERSION" + Log.i(TAG, "=== App started with SDK version: $SDK_VERSION ===") binding.btLogin.setOnClickListener { val email = binding.textEmail.text.toString() val password = binding.textPassword.text.toString() @@ -215,6 +223,7 @@ class DatabaseLoginFragment : Fragment() { } private suspend fun dbLoginAsync(email: String, password: String) { + Log.i(TAG, ">>> dbLoginAsync() called with email=$email") try { val result = authenticationApiClient.login(email, password, "Username-Password-Authentication") @@ -222,7 +231,21 @@ class DatabaseLoginFragment : Fragment() { .addParameter("scope", scope) .addParameter("audience", audience) .await() - credentialsManager.saveCredentials(result) + Log.i(TAG, ">>> dbLoginAsync SUCCESS - got credentials for ${result.user.name}") + Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") + try { + credentialsManager.saveCredentials(result) + Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) + } + Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") + try { + secureCredentialsManager.saveCredentials(result) + Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) + } Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -230,24 +253,40 @@ class DatabaseLoginFragment : Fragment() { ) .show() } catch (error: AuthenticationException) { + Log.e(TAG, ">>> dbLoginAsync FAILED", error) Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() } } private fun dbLogin(email: String, password: String) { + Log.i(TAG, ">>> dbLogin() called with email=$email") authenticationApiClient.login(email, password, "Username-Password-Authentication") .validateClaims() .addParameter("scope", scope) .addParameter("audience", audience) - //Additional customization to the request goes here .start(object : Callback { override fun onFailure(error: AuthenticationException) { + Log.e(TAG, ">>> dbLogin FAILED", error) Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG) .show() } override fun onSuccess(result: Credentials) { - credentialsManager.saveCredentials(result) + Log.i(TAG, ">>> dbLogin SUCCESS - got credentials for ${result.user.name}") + Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") + try { + credentialsManager.saveCredentials(result) + Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) + } + Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") + try { + secureCredentialsManager.saveCredentials(result) + Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) + } Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -258,14 +297,28 @@ class DatabaseLoginFragment : Fragment() { } private fun webAuth() { + Log.i(TAG, ">>> webAuth() called - starting browser login") WebAuthProvider.login(account) .withScheme(getString(R.string.com_auth0_scheme)) .withAudience(audience) .withScope(scope) .start(requireContext(), object : Callback { override fun onSuccess(result: Credentials) { - credentialsManager.saveCredentials(result) - secureCredentialsManager.saveCredentials(result) + Log.i(TAG, ">>> webAuth SUCCESS - got credentials for ${result.user.name}") + Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") + try { + credentialsManager.saveCredentials(result) + Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) + } + Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") + try { + secureCredentialsManager.saveCredentials(result) + Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") + } catch (e: Exception) { + Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) + } Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -274,6 +327,7 @@ class DatabaseLoginFragment : Fragment() { } override fun onFailure(error: AuthenticationException) { + Log.e(TAG, ">>> webAuth FAILED", error) val message = if (error.isCanceled) "Browser was closed" diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index 5d3731b25..96271d2dc 100644 --- a/sample/src/main/res/layout/fragment_database_login.xml +++ b/sample/src/main/res/layout/fragment_database_login.xml @@ -46,6 +46,22 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textEmail" /> + + + app:layout_constraintTop_toBottomOf="@+id/tvSdkVersion" /> Date: Fri, 6 Mar 2026 10:43:15 +0530 Subject: [PATCH 3/5] fix: Wrap ProviderException as CryptoException to enable key recovery on Android 12+ --- .../authentication/storage/CryptoUtil.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java index e6a5a0006..4ac745467 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java @@ -362,10 +362,17 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry throw new IncompatibleDeviceException(e); } catch (ProviderException e) { /* - * On Android 12+ (Keystore2), a padding mismatch throws ProviderException - * instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1 - * key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException - * allows the caller to fall through to key regeneration. + * - ProviderException: + * Thrown on Android 12+ (API 31+, Keystore2) when the RSA key's padding + * restriction does not match the cipher transformation. For example, an RSA + * key generated with ENCRYPTION_PADDING_RSA_PKCS1 will trigger this when + * initialised with an OAEPWithSHA-1AndMGF1Padding cipher. On API 23-30 the + * same condition surfaces as InvalidKeyException. + * + * This is NOT a device-level incompatibility -- the key can be deleted and + * regenerated with the correct padding. Wrapping as CryptoException (rather + * than IncompatibleDeviceException) ensures the caller falls through to key + * cleanup and regeneration instead of permanently blocking the user. */ Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e); deleteAESKeys(); @@ -423,10 +430,17 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry throw new IncompatibleDeviceException(e); } catch (ProviderException e) { /* - * On Android 12+ (Keystore2), a padding mismatch throws ProviderException - * instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1 - * key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException - * allows the caller to fall through to key regeneration. + * - ProviderException: + * Thrown on Android 12+ (API 31+, Keystore2) when the RSA key's padding + * restriction does not match the cipher transformation. For example, an RSA + * key generated with ENCRYPTION_PADDING_RSA_PKCS1 will trigger this when + * initialised with an OAEPWithSHA-1AndMGF1Padding cipher. On API 23-30 the + * same condition surfaces as InvalidKeyException. + * + * This is NOT a device-level incompatibility -- the key can be deleted and + * regenerated with the correct padding. Wrapping as CryptoException (rather + * than IncompatibleDeviceException) ensures the caller falls through to key + * cleanup and regeneration instead of permanently blocking the user. */ Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e); deleteAESKeys(); From 719cb1c91bb2e1c3bf168ef042718a8a651f6eb7 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 6 Mar 2026 10:47:17 +0530 Subject: [PATCH 4/5] fix: Revert sample app changes --- .../com/auth0/sample/DatabaseLoginFragment.kt | 64 ++----------------- .../res/layout/fragment_database_login.xml | 18 +----- 2 files changed, 6 insertions(+), 76 deletions(-) diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 81f01c7a3..918de531b 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -2,7 +2,6 @@ package com.auth0.sample import android.os.Bundle import android.os.CancellationSignal -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -51,11 +50,6 @@ import java.util.concurrent.Executors */ class DatabaseLoginFragment : Fragment() { - companion object { - private const val TAG = "Auth0CrashRepro" - private const val SDK_VERSION = "3.13.0 (OAEP)" - } - private val scope = "openid profile email read:current_user update:current_user_metadata" private val account: Auth0 by lazy { @@ -128,8 +122,6 @@ class DatabaseLoginFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val binding = FragmentDatabaseLoginBinding.inflate(inflater, container, false) - binding.tvSdkVersion.text = "SDK: $SDK_VERSION" - Log.i(TAG, "=== App started with SDK version: $SDK_VERSION ===") binding.btLogin.setOnClickListener { val email = binding.textEmail.text.toString() val password = binding.textPassword.text.toString() @@ -223,7 +215,6 @@ class DatabaseLoginFragment : Fragment() { } private suspend fun dbLoginAsync(email: String, password: String) { - Log.i(TAG, ">>> dbLoginAsync() called with email=$email") try { val result = authenticationApiClient.login(email, password, "Username-Password-Authentication") @@ -231,21 +222,7 @@ class DatabaseLoginFragment : Fragment() { .addParameter("scope", scope) .addParameter("audience", audience) .await() - Log.i(TAG, ">>> dbLoginAsync SUCCESS - got credentials for ${result.user.name}") - Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") - try { - credentialsManager.saveCredentials(result) - Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) - } - Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") - try { - secureCredentialsManager.saveCredentials(result) - Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) - } + credentialsManager.saveCredentials(result) Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -253,40 +230,24 @@ class DatabaseLoginFragment : Fragment() { ) .show() } catch (error: AuthenticationException) { - Log.e(TAG, ">>> dbLoginAsync FAILED", error) Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() } } private fun dbLogin(email: String, password: String) { - Log.i(TAG, ">>> dbLogin() called with email=$email") authenticationApiClient.login(email, password, "Username-Password-Authentication") .validateClaims() .addParameter("scope", scope) .addParameter("audience", audience) + //Additional customization to the request goes here .start(object : Callback { override fun onFailure(error: AuthenticationException) { - Log.e(TAG, ">>> dbLogin FAILED", error) Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG) .show() } override fun onSuccess(result: Credentials) { - Log.i(TAG, ">>> dbLogin SUCCESS - got credentials for ${result.user.name}") - Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") - try { - credentialsManager.saveCredentials(result) - Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) - } - Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") - try { - secureCredentialsManager.saveCredentials(result) - Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) - } + credentialsManager.saveCredentials(result) Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -297,28 +258,14 @@ class DatabaseLoginFragment : Fragment() { } private fun webAuth() { - Log.i(TAG, ">>> webAuth() called - starting browser login") WebAuthProvider.login(account) .withScheme(getString(R.string.com_auth0_scheme)) .withAudience(audience) .withScope(scope) .start(requireContext(), object : Callback { override fun onSuccess(result: Credentials) { - Log.i(TAG, ">>> webAuth SUCCESS - got credentials for ${result.user.name}") - Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...") - try { - credentialsManager.saveCredentials(result) - Log.i(TAG, ">>> credentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e) - } - Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...") - try { - secureCredentialsManager.saveCredentials(result) - Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK") - } catch (e: Exception) { - Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e) - } + credentialsManager.saveCredentials(result) + secureCredentialsManager.saveCredentials(result) Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -327,7 +274,6 @@ class DatabaseLoginFragment : Fragment() { } override fun onFailure(error: AuthenticationException) { - Log.e(TAG, ">>> webAuth FAILED", error) val message = if (error.isCanceled) "Browser was closed" diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index 96271d2dc..5d3731b25 100644 --- a/sample/src/main/res/layout/fragment_database_login.xml +++ b/sample/src/main/res/layout/fragment_database_login.xml @@ -46,22 +46,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textEmail" /> - - + app:layout_constraintTop_toTopOf="parent" /> Date: Tue, 10 Mar 2026 15:58:30 +0530 Subject: [PATCH 5/5] adding UT cases as per review comments --- .../storage/CryptoUtilTest.java | 169 +++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java index 55b1f507c..7e53a8962 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java @@ -1998,7 +1998,8 @@ public void shouldTriggerPKCS1MigrationWhenRSADecryptThrowsProviderException() t PowerMockito.when(Base64.encode(reEncryptedOAEP, Base64.DEFAULT)) .thenReturn(encodedOAEP.getBytes(StandardCharsets.UTF_8)); - doThrow(new IncompatibleDeviceException( + doThrow(new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", new ProviderException(new KeyStoreException("Incompatible padding mode")))) .when(cryptoUtil).RSADecrypt(encryptedAESPKCS1); @@ -2082,4 +2083,170 @@ public void shouldDeleteStaleRSAKeyAndRethrowOnIncompatibleDeviceExceptionDuring Mockito.verify(storage).remove(KEY_ALIAS); Mockito.verify(storage).remove(OLD_KEY_ALIAS); } + + @Test + public void shouldHandleProviderExceptionInAttemptPKCS1Migration() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + byte[] encryptedAESPKCS1 = new byte[]{10, 11, 12, 13}; + String encodedPKCS1 = "pkcs1_encoded"; + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(encodedPKCS1); + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESPKCS1); + + doThrow(new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", + new ProviderException(new KeyStoreException("Incompatible padding mode")))) + .when(cryptoUtil).RSADecrypt(encryptedAESPKCS1); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + KeyStore.PrivateKeyEntry mockEntry = mock(KeyStore.PrivateKeyEntry.class); + when(mockEntry.getPrivateKey()).thenReturn(mock(PrivateKey.class)); + when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) + .thenReturn(mockEntry); + when(rsaPkcs1Cipher.doFinal(encryptedAESPKCS1)) + .thenThrow(new ProviderException(new KeyStoreException("Incompatible padding mode"))); + + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + + byte[] newAESKey = new byte[32]; + Arrays.fill(newAESKey, (byte) 0xDD); + SecretKey mockSecret = mock(SecretKey.class); + when(mockSecret.getEncoded()).thenReturn(newAESKey); + when(keyGenerator.generateKey()).thenReturn(mockSecret); + + byte[] encryptedNewKey = new byte[]{30, 31, 32, 33}; + doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); + String encodedNewKey = "new_key_encoded"; + PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(newAESKey)); + Mockito.verify(storage).store(KEY_ALIAS, encodedNewKey); + } + + + @Test + public void shouldHandleProviderExceptionInTryMigrateLegacyAESKey() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); + + String encodedOldAES = "old_legacy_key"; + byte[] encryptedOldAES = new byte[]{1, 2, 3, 4}; + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(encodedOldAES); + + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedOldAES, Base64.DEFAULT)).thenReturn(encryptedOldAES); + + KeyStore.PrivateKeyEntry mockEntry = mock(KeyStore.PrivateKeyEntry.class); + PrivateKey mockPrivateKey = mock(PrivateKey.class); + when(mockEntry.getPrivateKey()).thenReturn(mockPrivateKey); + doReturn(mockEntry).when(cryptoUtil).getRSAKeyEntry(); + + when(rsaPkcs1Cipher.doFinal(encryptedOldAES)) + .thenThrow(new ProviderException(new KeyStoreException("Incompatible padding mode"))); + + byte[] newAESKey = new byte[32]; + Arrays.fill(newAESKey, (byte) 0xEE); + SecretKey mockSecret = mock(SecretKey.class); + when(mockSecret.getEncoded()).thenReturn(newAESKey); + when(keyGenerator.generateKey()).thenReturn(mockSecret); + + byte[] encryptedNewKey = new byte[]{40, 41, 42, 43}; + doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); + String encodedNewKey = "new_generated_key"; + PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(newAESKey)); + Mockito.verify(storage).store(KEY_ALIAS, encodedNewKey); + } + + + @Test + public void shouldFallThroughToKeyRegenerationWhenMigrationFailsWithCryptoException() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + byte[] encryptedAESPKCS1 = new byte[]{10, 11, 12, 13}; + String encodedPKCS1 = "pkcs1_encoded"; + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(encodedPKCS1); + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESPKCS1); + + doThrow(new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", + new ProviderException(new KeyStoreException("Incompatible padding mode")))) + .when(cryptoUtil).RSADecrypt(encryptedAESPKCS1); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + when(keyStore.containsAlias(OLD_KEY_ALIAS)).thenReturn(false); + + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + + byte[] newAESKey = new byte[32]; + Arrays.fill(newAESKey, (byte) 0xFF); + SecretKey mockSecret = mock(SecretKey.class); + when(mockSecret.getEncoded()).thenReturn(newAESKey); + when(keyGenerator.generateKey()).thenReturn(mockSecret); + + byte[] encryptedNewKey = new byte[]{50, 51, 52, 53}; + doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); + String encodedNewKey = "regenerated_key"; + PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(newAESKey)); + Mockito.verify(storage).store(KEY_ALIAS, encodedNewKey); + Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); + Mockito.verify(keyStore).deleteEntry(OLD_KEY_ALIAS); + } + + @Test + public void shouldNotPropagateProviderExceptionAsIncompatibleDeviceException() throws Exception { + CryptoUtil cryptoUtil = newCryptoUtilSpy(); + + byte[] encryptedAESPKCS1 = new byte[]{10, 11, 12, 13}; + String encodedPKCS1 = "pkcs1_encoded"; + + when(storage.retrieveString(KEY_ALIAS)).thenReturn(encodedPKCS1); + PowerMockito.mockStatic(Base64.class); + PowerMockito.when(Base64.decode(encodedPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESPKCS1); + + doThrow(new CryptoException( + "The RSA key's padding mode is incompatible with the current cipher.", + new ProviderException(new KeyStoreException("Incompatible padding mode")))) + .when(cryptoUtil).RSADecrypt(encryptedAESPKCS1); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + when(keyStore.containsAlias(OLD_KEY_ALIAS)).thenReturn(false); + + when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + + byte[] newAESKey = new byte[32]; + Arrays.fill(newAESKey, (byte) 0xAA); + SecretKey mockSecret = mock(SecretKey.class); + when(mockSecret.getEncoded()).thenReturn(newAESKey); + when(keyGenerator.generateKey()).thenReturn(mockSecret); + + byte[] encryptedNewKey = new byte[]{60, 61, 62, 63}; + doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); + String encodedNewKey = "recovered_key"; + PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + + byte[] result = cryptoUtil.getAESKey(); + + assertThat(result, is(notNullValue())); + assertThat(result, is(newAESKey)); + + } }