Skip to content

Commit 15eb856

Browse files
committed
fix: Wrap ProviderException as CryptoException to enable key recovery on Android 12+
1 parent 43f9358 commit 15eb856

4 files changed

Lines changed: 111 additions & 30 deletions

File tree

auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,8 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry
344344
cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_SPEC);
345345
return cipher.doFinal(encryptedInput);
346346
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
347-
| InvalidAlgorithmParameterException | ProviderException e) {
347+
| InvalidAlgorithmParameterException e) {
348348
/*
349-
* This exceptions are safe to be ignored:
350-
*
351349
* - NoSuchPaddingException:
352350
* Thrown if PKCS1Padding is not available. Was introduced in API 1.
353351
* - NoSuchAlgorithmException:
@@ -357,16 +355,22 @@ byte[] RSADecrypt(byte[] encryptedInput) throws IncompatibleDeviceException, Cry
357355
* Thrown if the given key is inappropriate for initializing this cipher.
358356
* - InvalidAlgorithmParameterException:
359357
* Thrown if the OAEP parameters are invalid or unsupported.
360-
* - ProviderException:
361-
* Thrown on Android 12+ (Keystore2) when the key's padding restriction is
362-
* incompatible with the cipher transformation (e.g. a PKCS1-restricted key
363-
* initialised with an OAEP spec). On Android < 12 this surfaces as
364-
* InvalidKeyException instead.
365358
*
366359
* Read more in https://developer.android.com/reference/javax/crypto/Cipher
367360
*/
368361
Log.e(TAG, "The device can't decrypt input using a RSA Key.", e);
369362
throw new IncompatibleDeviceException(e);
363+
} catch (ProviderException e) {
364+
/*
365+
* On Android 12+ (Keystore2), a padding mismatch throws ProviderException
366+
* instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1
367+
* key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException
368+
* allows the caller to fall through to key regeneration.
369+
*/
370+
Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e);
371+
deleteAESKeys();
372+
throw new CryptoException(
373+
"The RSA key's padding mode is incompatible with the current cipher.", e);
370374
} catch (IllegalArgumentException | IllegalBlockSizeException | BadPaddingException e) {
371375
/*
372376
* 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
401405
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey(), OAEP_SPEC);
402406
return cipher.doFinal(decryptedInput);
403407
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
404-
| InvalidAlgorithmParameterException | ProviderException e) {
408+
| InvalidAlgorithmParameterException e) {
405409
/*
406-
* This exceptions are safe to be ignored:
407-
*
408410
* - NoSuchPaddingException:
409411
* Thrown if PKCS1Padding is not available. Was introduced in API 1.
410412
* - NoSuchAlgorithmException:
@@ -414,16 +416,22 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry
414416
* Thrown if the given key is inappropriate for initializing this cipher.
415417
* - InvalidAlgorithmParameterException:
416418
* Thrown if the OAEP parameters are invalid or unsupported.
417-
* - ProviderException:
418-
* Thrown on Android 12+ (Keystore2) when the key's padding restriction is
419-
* incompatible with the cipher transformation (e.g. a PKCS1-restricted key
420-
* initialised with an OAEP spec). On Android < 12 this surfaces as
421-
* InvalidKeyException instead.
422419
*
423420
* Read more in https://developer.android.com/reference/javax/crypto/Cipher
424421
*/
425422
Log.e(TAG, "The device can't encrypt input using a RSA Key.", e);
426423
throw new IncompatibleDeviceException(e);
424+
} catch (ProviderException e) {
425+
/*
426+
* On Android 12+ (Keystore2), a padding mismatch throws ProviderException
427+
* instead of InvalidKeyException. This is a KEY incompatibility (stale PKCS1
428+
* key with OAEP cipher), not a DEVICE incompatibility. Wrapping as CryptoException
429+
* allows the caller to fall through to key regeneration.
430+
*/
431+
Log.e(TAG, "RSA key padding mismatch detected (Android 12+ Keystore2).", e);
432+
deleteAESKeys();
433+
throw new CryptoException(
434+
"The RSA key's padding mode is incompatible with the current cipher.", e);
427435
} catch (IllegalBlockSizeException | BadPaddingException e) {
428436
/*
429437
* They really should not be thrown at all since padding is requested in the transformation.
@@ -479,10 +487,12 @@ private byte[] attemptPKCS1Migration(byte[] encryptedAESBytes) {
479487

480488
} catch (BadPaddingException | IllegalBlockSizeException e) {
481489
Log.e(TAG, "PKCS1 decryption failed. Data may be corrupted.", e);
482-
} catch (KeyStoreException | CertificateException | IOException |
490+
} catch (KeyStoreException | CertificateException | IOException |
483491
NoSuchAlgorithmException | UnrecoverableEntryException |
484492
NoSuchPaddingException | InvalidKeyException e) {
485493
Log.e(TAG, "Migration failed due to key access error.", e);
494+
} catch (ProviderException e) {
495+
Log.e(TAG, "PKCS1 migration failed: key padding incompatible (Android 12+ Keystore2).", e);
486496
} catch (CryptoException e) {
487497
Log.e(TAG, "Failed to re-encrypt AES key with OAEP.", e);
488498
}
@@ -617,7 +627,8 @@ private byte[] tryMigrateLegacyAESKey() {
617627
Log.d(TAG, "Legacy AES key migrated successfully");
618628
return decryptedAESKey;
619629
} catch (CryptoException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException |
620-
BadPaddingException | IllegalBlockSizeException | IllegalArgumentException e) {
630+
BadPaddingException | IllegalBlockSizeException | IllegalArgumentException |
631+
ProviderException e) {
621632
Log.e(TAG, "Could not migrate legacy AES key. Will generate new key.", e);
622633
deleteAESKeys();
623634
return null;

auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,9 +1944,9 @@ public void shouldGenerateNewKeyWhenMigrationFails() throws Exception {
19441944
}
19451945

19461946
@Test
1947-
public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsIncompatibleDevice() {
1948-
Assert.assertThrows("The device is not compatible with the CryptoUtil class",
1949-
IncompatibleDeviceException.class, () -> {
1947+
public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsCryptoException() {
1948+
Assert.assertThrows("The RSA key's padding mode is incompatible with the current cipher.",
1949+
CryptoException.class, () -> {
19501950
PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
19511951
KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
19521952
doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
@@ -1962,9 +1962,9 @@ public void shouldWrapProviderExceptionFromCipherInitInRSADecryptAsIncompatibleD
19621962
}
19631963

19641964
@Test
1965-
public void shouldWrapProviderExceptionFromCipherInitInRSAEncryptAsIncompatibleDevice() {
1966-
Assert.assertThrows("The device is not compatible with the CryptoUtil class",
1967-
IncompatibleDeviceException.class, () -> {
1965+
public void shouldWrapProviderExceptionFromCipherInitInRSAEncryptAsCryptoException() {
1966+
Assert.assertThrows("The RSA key's padding mode is incompatible with the current cipher.",
1967+
CryptoException.class, () -> {
19681968
PublicKey publicKey = PowerMockito.mock(PublicKey.class);
19691969
Certificate certificate = PowerMockito.mock(Certificate.class);
19701970
doReturn(publicKey).when(certificate).getPublicKey();

sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.auth0.sample
22

33
import android.os.Bundle
44
import android.os.CancellationSignal
5+
import android.util.Log
56
import android.view.LayoutInflater
67
import android.view.View
78
import android.view.ViewGroup
@@ -50,6 +51,11 @@ import java.util.concurrent.Executors
5051
*/
5152
class DatabaseLoginFragment : Fragment() {
5253

54+
companion object {
55+
private const val TAG = "Auth0CrashRepro"
56+
private const val SDK_VERSION = "3.13.0 (OAEP)"
57+
}
58+
5359
private val scope = "openid profile email read:current_user update:current_user_metadata"
5460

5561
private val account: Auth0 by lazy {
@@ -122,6 +128,8 @@ class DatabaseLoginFragment : Fragment() {
122128
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
123129
): View {
124130
val binding = FragmentDatabaseLoginBinding.inflate(inflater, container, false)
131+
binding.tvSdkVersion.text = "SDK: $SDK_VERSION"
132+
Log.i(TAG, "=== App started with SDK version: $SDK_VERSION ===")
125133
binding.btLogin.setOnClickListener {
126134
val email = binding.textEmail.text.toString()
127135
val password = binding.textPassword.text.toString()
@@ -215,39 +223,70 @@ class DatabaseLoginFragment : Fragment() {
215223
}
216224

217225
private suspend fun dbLoginAsync(email: String, password: String) {
226+
Log.i(TAG, ">>> dbLoginAsync() called with email=$email")
218227
try {
219228
val result =
220229
authenticationApiClient.login(email, password, "Username-Password-Authentication")
221230
.validateClaims()
222231
.addParameter("scope", scope)
223232
.addParameter("audience", audience)
224233
.await()
225-
credentialsManager.saveCredentials(result)
234+
Log.i(TAG, ">>> dbLoginAsync SUCCESS - got credentials for ${result.user.name}")
235+
Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...")
236+
try {
237+
credentialsManager.saveCredentials(result)
238+
Log.i(TAG, ">>> credentialsManager.saveCredentials() OK")
239+
} catch (e: Exception) {
240+
Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e)
241+
}
242+
Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...")
243+
try {
244+
secureCredentialsManager.saveCredentials(result)
245+
Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK")
246+
} catch (e: Exception) {
247+
Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e)
248+
}
226249
Snackbar.make(
227250
requireView(),
228251
"Hello ${result.user.name}",
229252
Snackbar.LENGTH_LONG
230253
)
231254
.show()
232255
} catch (error: AuthenticationException) {
256+
Log.e(TAG, ">>> dbLoginAsync FAILED", error)
233257
Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show()
234258
}
235259
}
236260

237261
private fun dbLogin(email: String, password: String) {
262+
Log.i(TAG, ">>> dbLogin() called with email=$email")
238263
authenticationApiClient.login(email, password, "Username-Password-Authentication")
239264
.validateClaims()
240265
.addParameter("scope", scope)
241266
.addParameter("audience", audience)
242-
//Additional customization to the request goes here
243267
.start(object : Callback<Credentials, AuthenticationException> {
244268
override fun onFailure(error: AuthenticationException) {
269+
Log.e(TAG, ">>> dbLogin FAILED", error)
245270
Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG)
246271
.show()
247272
}
248273

249274
override fun onSuccess(result: Credentials) {
250-
credentialsManager.saveCredentials(result)
275+
Log.i(TAG, ">>> dbLogin SUCCESS - got credentials for ${result.user.name}")
276+
Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...")
277+
try {
278+
credentialsManager.saveCredentials(result)
279+
Log.i(TAG, ">>> credentialsManager.saveCredentials() OK")
280+
} catch (e: Exception) {
281+
Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e)
282+
}
283+
Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...")
284+
try {
285+
secureCredentialsManager.saveCredentials(result)
286+
Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK")
287+
} catch (e: Exception) {
288+
Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e)
289+
}
251290
Snackbar.make(
252291
requireView(),
253292
"Hello ${result.user.name}",
@@ -258,14 +297,28 @@ class DatabaseLoginFragment : Fragment() {
258297
}
259298

260299
private fun webAuth() {
300+
Log.i(TAG, ">>> webAuth() called - starting browser login")
261301
WebAuthProvider.login(account)
262302
.withScheme(getString(R.string.com_auth0_scheme))
263303
.withAudience(audience)
264304
.withScope(scope)
265305
.start(requireContext(), object : Callback<Credentials, AuthenticationException> {
266306
override fun onSuccess(result: Credentials) {
267-
credentialsManager.saveCredentials(result)
268-
secureCredentialsManager.saveCredentials(result)
307+
Log.i(TAG, ">>> webAuth SUCCESS - got credentials for ${result.user.name}")
308+
Log.i(TAG, ">>> Calling credentialsManager.saveCredentials()...")
309+
try {
310+
credentialsManager.saveCredentials(result)
311+
Log.i(TAG, ">>> credentialsManager.saveCredentials() OK")
312+
} catch (e: Exception) {
313+
Log.e(TAG, ">>> credentialsManager.saveCredentials() FAILED", e)
314+
}
315+
Log.i(TAG, ">>> Calling secureCredentialsManager.saveCredentials()...")
316+
try {
317+
secureCredentialsManager.saveCredentials(result)
318+
Log.i(TAG, ">>> secureCredentialsManager.saveCredentials() OK")
319+
} catch (e: Exception) {
320+
Log.e(TAG, ">>> secureCredentialsManager.saveCredentials() FAILED", e)
321+
}
269322
Snackbar.make(
270323
requireView(),
271324
"Hello ${result.user.name}",
@@ -274,6 +327,7 @@ class DatabaseLoginFragment : Fragment() {
274327
}
275328

276329
override fun onFailure(error: AuthenticationException) {
330+
Log.e(TAG, ">>> webAuth FAILED", error)
277331
val message =
278332
if (error.isCanceled)
279333
"Browser was closed"

sample/src/main/res/layout/fragment_database_login.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@
4646
app:layout_constraintStart_toStartOf="parent"
4747
app:layout_constraintTop_toBottomOf="@+id/textEmail" />
4848

49+
<TextView
50+
android:id="@+id/tvSdkVersion"
51+
android:layout_width="match_parent"
52+
android:layout_height="wrap_content"
53+
android:layout_marginTop="4dp"
54+
android:gravity="center"
55+
android:padding="8dp"
56+
android:background="#FFEB3B"
57+
android:text="SDK Version: ..."
58+
android:textSize="14sp"
59+
android:textStyle="bold"
60+
android:textColor="#000000"
61+
app:layout_constraintEnd_toEndOf="parent"
62+
app:layout_constraintStart_toStartOf="parent"
63+
app:layout_constraintTop_toTopOf="parent" />
64+
4965
<TextView
5066
android:id="@+id/textView"
5167
android:layout_width="wrap_content"
@@ -58,7 +74,7 @@
5874
app:layout_constraintBottom_toTopOf="@+id/textEmail"
5975
app:layout_constraintEnd_toEndOf="parent"
6076
app:layout_constraintStart_toStartOf="parent"
61-
app:layout_constraintTop_toTopOf="parent" />
77+
app:layout_constraintTop_toBottomOf="@+id/tvSdkVersion" />
6278

6379
<TextView
6480
android:id="@+id/textView3"

0 commit comments

Comments
 (0)