Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,33 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
X500Principal principal = new X500Principal("CN=Auth0.Android,O=Auth0");

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
spec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
KeyGenParameterSpec.Builder specBuilder = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
.setCertificateSubject(principal)
.setCertificateSerialNumber(BigInteger.ONE)
.setCertificateNotBefore(start.getTime())
.setCertificateNotAfter(end.getTime())
.setKeySize(RSA_KEY_SIZE)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.build();
.setBlockModes(KeyProperties.BLOCK_MODE_ECB);

/*
* OAEP uses two independent digests: the OAEP digest (authorized above) and the MGF1
* digest. Our OAEP_SPEC uses MGF1/SHA-1. The setMgf1Digests API only exists on
* API 35+ (VANILLA_ICE_CREAM); below that KeyMint defaults the MGF1 digest
* (historically to SHA-1), so leaving it unset worked. On newer Keystore2 hardware
* the authorized MGF1 digest set is strictly enforced and its default is
* vendor-dependent (some default to SHA-256 only), which rejects our MGF1/SHA-1
* cipher with INCOMPATIBLE_MGF_DIGEST ("Incompatible padding mode"). Where the API is
* available, authorize SHA-1 (used by the current cipher) and SHA-256 explicitly so
* the key no longer depends on the vendor default and remains compatible if the
* cipher moves to MGF1/SHA-256 in the future. See OAEP_SPEC.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
specBuilder.setMgf1Digests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256);
}

spec = specBuilder.build();
} else {
//Following code is for API 18-22
//Generate new RSA KeyPair and save it on the KeyStore
Expand Down Expand Up @@ -343,18 +360,42 @@ 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 (InvalidKeyException | InvalidAlgorithmParameterException e) {
/*
* - InvalidKeyException:
* Thrown if the stored key is inappropriate for initializing this cipher. On newer
* Keystore2 hardware (API 35+) an existing RSA key whose authorized MGF1 digest set
* does not include SHA-1 rejects our MGF1/SHA-1 cipher here, surfacing as
* "Keystore operation failed" caused by INCOMPATIBLE_MGF_DIGEST ("Incompatible
* padding mode"). Such a key was generated by an older SDK before the MGF1 digest
* was authorized explicitly (see getRSAKeyEntry).
* - InvalidAlgorithmParameterException:
* Thrown if the OAEP parameters are invalid or unsupported for this key.
*
* This is NOT a device-level incompatibility -- the key can be deleted and regenerated
* with the MGF1 digest authorized. Delete the RSA key (and the AES key it protected,
* which is now unrecoverable) and wrap as CryptoException so the caller falls through to
* key regeneration instead of permanently blocking the user with INCOMPATIBLE_DEVICE.
* If the device genuinely cannot generate a compatible key, the subsequent
* getRSAKeyEntry() will throw IncompatibleDeviceException, preserving the terminal path.
*
* Read more in https://developer.android.com/reference/javax/crypto/Cipher
*/
Log.e(TAG, "RSA key rejected the cipher parameters (likely MGF1 digest mismatch on Keystore2). Recreating key.", e);
deleteRSAKeys();
deleteAESKeys();
throw new CryptoException(
"The RSA key's authorized parameters are incompatible with the current cipher. The keys have been recreated; please retry.", e);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
/*
* - NoSuchPaddingException:
* Thrown if PKCS1Padding is not available. Was introduced in API 1.
* - NoSuchAlgorithmException:
* Thrown if the transformation is null, empty or invalid, or if no security provider
* implements it. Was introduced in API 1.
* - InvalidKeyException:
* Thrown if the given key is inappropriate for initializing this cipher.
* - InvalidAlgorithmParameterException:
* Thrown if the OAEP parameters are invalid or unsupported.
*
* These indicate the algorithm/padding is not available on the device at all -- a real
* device-level incompatibility.
*
* Read more in https://developer.android.com/reference/javax/crypto/Cipher
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -898,8 +898,8 @@ public void shouldRSADecryptData() throws Exception {
}

@Test
public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSADecrypt() {
Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> {
public void shouldRecreateKeysAndThrowCryptoExceptionOnInvalidKeyExceptionWhenTryingToRSADecrypt() throws Exception {
Assert.assertThrows("The RSA key's authorized parameters are incompatible with the current cipher. The keys have been recreated; please retry.", CryptoException.class, () -> {
byte[] sampleBytes = new byte[0];
PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
Expand All @@ -911,6 +911,34 @@ public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSADecrypt() {

cryptoUtil.RSADecrypt(sampleBytes);
});

Mockito.verify(keyStore).deleteEntry(KEY_ALIAS);
Mockito.verify(keyStore).deleteEntry(OLD_KEY_ALIAS);
Mockito.verify(storage).remove(KEY_ALIAS);
Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
Mockito.verify(storage).remove(OLD_KEY_ALIAS);
Mockito.verify(storage).remove(OLD_KEY_ALIAS + "_iv");
}

@Test
public void shouldRecreateKeysAndThrowCryptoExceptionOnInvalidAlgorithmParameterExceptionWhenTryingToRSADecrypt() throws Exception {
Assert.assertThrows("The RSA key's authorized parameters are incompatible with the current cipher. The keys have been recreated; please retry.", CryptoException.class, () -> {
byte[] sampleBytes = new byte[0];
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 InvalidAlgorithmParameterException()).when(rsaOaepCipher).init(eq(Cipher.DECRYPT_MODE), eq(privateKey), any(AlgorithmParameterSpec.class));

cryptoUtil.RSADecrypt(sampleBytes);
});

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

@Test
Expand Down
Loading