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 1d6fe95df..bfa82a6b2 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
@@ -53,16 +53,17 @@ class CryptoUtil {
// Transformations available since API 18
// https://developer.android.com/training/articles/keystore.html#SupportedCiphers
- private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
+ private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
// https://developer.android.com/reference/javax/crypto/Cipher.html
@SuppressWarnings("SpellCheckingInspection")
private static final String AES_TRANSFORMATION = "AES/GCM/NOPADDING";
+ private static final String OLD_PKCS1_RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String ALGORITHM_RSA = "RSA";
private static final String ALGORITHM_AES = "AES";
private static final int AES_KEY_SIZE = 256;
- private static final int RSA_KEY_SIZE = 2048;
+ private static final int RSA_KEY_SIZE = 4096;
private final String OLD_KEY_ALIAS;
private final String OLD_KEY_IV_ALIAS;
@@ -124,7 +125,8 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
.setCertificateNotBefore(start.getTime())
.setCertificateNotAfter(end.getTime())
.setKeySize(RSA_KEY_SIZE)
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
+ .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA1)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.build();
} else {
@@ -355,6 +357,7 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry
/**
* Attempts to recover the existing AES Key or generates a new one if none is found.
+ * Handles migration from PKCS1Padding-encrypted AES keys to OAEP-encrypted ones.
*
* @return a valid AES Key bytes
* @throws IncompatibleDeviceException in the event the device can't understand the cryptographic settings required
@@ -363,41 +366,45 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry
@VisibleForTesting
byte[] getAESKey() throws IncompatibleDeviceException, CryptoException {
String encodedEncryptedAES = storage.retrieveString(KEY_ALIAS);
- if (TextUtils.isEmpty(encodedEncryptedAES)) {
- encodedEncryptedAES = storage.retrieveString(OLD_KEY_ALIAS);
+ if (!TextUtils.isEmpty(encodedEncryptedAES)) {
+ byte[] encryptedAESBytes = Base64.decode(encodedEncryptedAES, Base64.DEFAULT);
+ return RSADecrypt(encryptedAESBytes);
}
- if (encodedEncryptedAES != null) {
- //Return existing key
- byte[] encryptedAES = Base64.decode(encodedEncryptedAES, Base64.DEFAULT);
- byte[] existingAES = RSADecrypt(encryptedAES);
- final int aesExpectedLengthInBytes = AES_KEY_SIZE / 8;
- //Prevent returning an 'Empty key' (invalid/corrupted) that was mistakenly saved
- if (existingAES != null && existingAES.length == aesExpectedLengthInBytes) {
- //Key exists and has the right size
- return existingAES;
+ String encodedOldAES = storage.retrieveString(OLD_KEY_ALIAS);
+ if (!TextUtils.isEmpty(encodedOldAES)) {
+ try {
+ byte[] encryptedOldAESBytes = Base64.decode(encodedOldAES, Base64.DEFAULT);
+ KeyStore.PrivateKeyEntry rsaKeyEntry = getRSAKeyEntry();
+ Cipher rsaPkcs1Cipher = Cipher.getInstance(OLD_PKCS1_RSA_TRANSFORMATION);
+ rsaPkcs1Cipher.init(Cipher.DECRYPT_MODE, rsaKeyEntry.getPrivateKey());
+ byte[] decryptedAESKey = rsaPkcs1Cipher.doFinal(encryptedOldAESBytes);
+
+ byte[] encryptedAESWithOAEP = RSAEncrypt(decryptedAESKey);
+ String newEncodedEncryptedAES = new String(Base64.encode(encryptedAESWithOAEP, Base64.DEFAULT), StandardCharsets.UTF_8);
+ storage.store(KEY_ALIAS, newEncodedEncryptedAES);
+ storage.remove(OLD_KEY_ALIAS);
+ return decryptedAESKey;
+ } catch (Exception e) {
+ Log.e(TAG, "Could not migrate the legacy AES key. A new key will be generated.", e);
+ deleteAESKeys();
}
}
- //Key doesn't exist. Generate new AES
+
try {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM_AES);
keyGen.init(AES_KEY_SIZE);
- byte[] aes = keyGen.generateKey().getEncoded();
- //Save encrypted encoded version
- byte[] encryptedAES = RSAEncrypt(aes);
- String encodedEncryptedAESText = new String(Base64.encode(encryptedAES, Base64.DEFAULT), StandardCharsets.UTF_8);
- storage.store(KEY_ALIAS, encodedEncryptedAESText);
- return aes;
+ byte[] decryptedAESKey = keyGen.generateKey().getEncoded();
+
+ byte[] encryptedNewAES = RSAEncrypt(decryptedAESKey);
+ String encodedEncryptedNewAESText = new String(Base64.encode(encryptedNewAES, Base64.DEFAULT), StandardCharsets.UTF_8);
+ storage.store(KEY_ALIAS, encodedEncryptedNewAESText);
+ return decryptedAESKey;
} catch (NoSuchAlgorithmException e) {
- /*
- * This exceptions are safe to be ignored:
- *
- * - NoSuchAlgorithmException:
- * Thrown if the Algorithm implementation is not available. AES was introduced in API 1
- *
- * Read more in https://developer.android.com/reference/javax/crypto/KeyGenerator
- */
- Log.e(TAG, "Error while creating the AES key.", e);
+ Log.e(TAG, "Error while creating the new AES key.", e);
throw new IncompatibleDeviceException(e);
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error while creating the new AES key.", e);
+ throw new CryptoException("Unexpected error while creating the 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 d38035bdd..5269b91ad 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
@@ -21,12 +21,12 @@
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
-import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import java.io.IOException;
import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
@@ -42,7 +42,6 @@
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.Date;
-import java.util.concurrent.TimeUnit;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -51,443 +50,178 @@
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.x500.X500Principal;
+import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.greaterThan;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.doNothing;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.doThrow;
import static org.powermock.api.mockito.PowerMockito.mock;
+import static org.powermock.api.mockito.PowerMockito.verifyPrivate;
+import static org.powermock.api.mockito.PowerMockito.whenNew;
-/**
- * In the rest of the test files we use Mockito as that's enough for most cases. However,
- * when Kotlin classes are introduced in the project, Mockito fails to mock them because
- * they are final by default.
- * The solution is to use the 'mockito-inline' plugin. However, when used in combination
- * with Powermock, both configuration files clash and the tests fail.
- * The MockMaker needs to be set up only in one place, the Powermock configuration file.
- *
- * Read more: https://github.com/powermock/powermock/issues/992#issuecomment-662845804
- */
@RunWith(PowerMockRunner.class)
-@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class})
+@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class, KeyStore.class, KeyPairGenerator.class})
public class CryptoUtilTest {
- private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
+ private static final String NEW_RSA_OAEP_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
+ private static final String OLD_RSA_PKCS1_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_TRANSFORMATION = "AES/GCM/NOPADDING";
private static final String CERTIFICATE_PRINCIPAL = "CN=Auth0.Android,O=Auth0";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String ALGORITHM_AES = "AES";
private static final String ALGORITHM_RSA = "RSA";
-
- private final Storage storage = PowerMockito.mock(Storage.class);
- private final Cipher rsaCipher = PowerMockito.mock(Cipher.class);
- private final Cipher aesCipher = PowerMockito.mock(Cipher.class);
- private final KeyStore keyStore = PowerMockito.mock(KeyStore.class);
- private final KeyPairGenerator keyPairGenerator = PowerMockito.mock(KeyPairGenerator.class);
- private final KeyGenerator keyGenerator = PowerMockito.mock(KeyGenerator.class);
-
- private CryptoUtil cryptoUtil;
+ private static final int AES_KEY_SIZE = 256;
+ private static final int RSA_KEY_SIZE = 2048;
private static final String APP_PACKAGE_NAME = "com.mycompany.myapp";
private static final String BASE_ALIAS = "keyName";
private static final String KEY_ALIAS = APP_PACKAGE_NAME + "." + BASE_ALIAS;
private static final String OLD_KEY_ALIAS = BASE_ALIAS;
+ private static final String KEY_IV_ALIAS = KEY_ALIAS + "_iv";
+ private static final String OLD_KEY_IV_ALIAS = OLD_KEY_ALIAS + "_iv";
+
+ private Storage storage;
+ private Cipher rsaOaepCipher;
+ private Cipher rsaPkcs1Cipher;
+ private Cipher aesCipher;
+ private KeyStore keyStore;
+ private KeyPairGenerator keyPairGenerator;
+ private KeyGenerator keyGenerator;
+ private CryptoUtil cryptoUtil;
private Context context;
-
- //Android KeyStore not accessible using Robolectric
- //Must test using white-box approach
- //Ref: https://github.com/robolectric/robolectric/issues/1518
+ private KeyStore.PrivateKeyEntry mockRsaPrivateKeyEntry;
+ private PrivateKey mockRsaPrivateKey;
+ private Certificate mockRsaCertificate;
+ private SecretKey mockAesSecretKey;
+ private byte[] testAesKeyBytes;
+ private byte[] testPlaintextData;
+ private byte[] testCiphertextData;
+ private byte[] testIvBytes;
+ private String testEncodedIv;
@Before
public void setUp() throws Exception {
PowerMockito.mockStatic(Log.class);
PowerMockito.mockStatic(TextUtils.class);
- PowerMockito.when(TextUtils.isEmpty(anyString())).then((Answer) invocation -> {
+ when(TextUtils.isEmpty(anyString())).then((Answer) invocation -> {
String input = invocation.getArgument(0, String.class);
return input == null || input.isEmpty();
});
+ PowerMockito.mockStatic(Base64.class);
+ PowerMockito.mockStatic(KeyStore.class);
+ PowerMockito.mockStatic(KeyPairGenerator.class);
+ PowerMockito.mockStatic(KeyGenerator.class);
+ PowerMockito.mockStatic(Cipher.class);
+
+ storage = PowerMockito.mock(Storage.class);
+ rsaOaepCipher = PowerMockito.mock(Cipher.class);
+ rsaPkcs1Cipher = PowerMockito.mock(Cipher.class);
+ aesCipher = PowerMockito.mock(Cipher.class);
+ keyStore = PowerMockito.mock(KeyStore.class);
+ keyPairGenerator = PowerMockito.mock(KeyPairGenerator.class);
+ keyGenerator = PowerMockito.mock(KeyGenerator.class);
context = mock(Context.class);
when(context.getPackageName()).thenReturn(APP_PACKAGE_NAME);
- cryptoUtil = newCryptoUtilSpy();
- }
-
- /*
- * GET RSA KEY tests
- */
-
- @Test
- public void shouldThrowWhenRSAKeyAliasIsInvalid() {
- Assert.assertThrows("RSA and AES Key alias must be valid.", IllegalArgumentException.class, () -> {
- //noinspection deprecation
- new CryptoUtil(RuntimeEnvironment.application, storage, " ");
+ when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(mock(KeyguardManager.class));
+ when(context.getApplicationContext()).thenReturn(mock(Context.class));
+
+ mockRsaPrivateKey = PowerMockito.mock(PrivateKey.class);
+ mockRsaCertificate = PowerMockito.mock(Certificate.class);
+ mockRsaPrivateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
+ when(mockRsaPrivateKeyEntry.getPrivateKey()).thenReturn(mockRsaPrivateKey);
+ when(mockRsaPrivateKeyEntry.getCertificate()).thenReturn(mockRsaCertificate);
+
+ testAesKeyBytes = new byte[AES_KEY_SIZE / 8];
+ for (int i = 0; i < testAesKeyBytes.length; i++) testAesKeyBytes[i] = (byte) i;
+ mockAesSecretKey = new SecretKeySpec(testAesKeyBytes, ALGORITHM_AES);
+
+ testPlaintextData = "auth0-rocks".getBytes(StandardCharsets.UTF_8);
+ testCiphertextData = "encrypted-auth0-rocks".getBytes(StandardCharsets.UTF_8);
+ testIvBytes = new byte[12];
+ for (int i = 0; i < testIvBytes.length; i++) testIvBytes[i] = (byte) (i + 10);
+ testEncodedIv = "base64EncodedGCMIV==";
+ PowerMockito.when(Base64.encode(any(byte[].class), anyInt())).thenAnswer((Answer) invocation -> {
+ byte[] input = invocation.getArgument(0);
+ if (Arrays.equals(input, testIvBytes)) {
+ return testEncodedIv.getBytes(StandardCharsets.UTF_8);
+ }
+ return java.util.Base64.getEncoder().encode(input);
+ });
+ PowerMockito.when(Base64.decode(anyString(), anyInt())).thenAnswer((Answer) invocation -> {
+ String input = invocation.getArgument(0);
+ if (input == null) return new byte[0];
+ if (testEncodedIv.equals(input)) return testIvBytes;
+ try {
+ return java.util.Base64.getDecoder().decode(input);
+ } catch (Exception e) {
+ return input.getBytes(StandardCharsets.UTF_8);
+ }
});
- }
-
- @Test
- @Config(sdk = 19)
- public void shouldNotCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI19() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19);
-
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
- KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
- PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
- //Set LockScreen as Enabled
- KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
- PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
- PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true);
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setSubject(principalCaptor.capture());
- Mockito.verify(builder).setAlias(KEY_ALIAS);
- Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setStartDate(startDateCaptor.capture());
- Mockito.verify(builder).setEndDate(endDateCaptor.capture());
- Mockito.verify(builder, never()).setEncryptionRequired();
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
-
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
-
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
-
- assertThat(entry, is(expectedEntry));
- }
-
- @Test
- @Config(sdk = 21)
- public void shouldCreateUnprotectedRSAKeyPairIfMissingAndLockScreenDisabledOnAPI21() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21);
-
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
- KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
- PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
- //Set LockScreen as Disabled
- KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
- PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
- PowerMockito.when(kService.isKeyguardSecure()).thenReturn(false);
- PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(CharSequence.class), any(CharSequence.class))).thenReturn(null);
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setSubject(principalCaptor.capture());
- Mockito.verify(builder).setAlias(KEY_ALIAS);
- Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setStartDate(startDateCaptor.capture());
- Mockito.verify(builder).setEndDate(endDateCaptor.capture());
- Mockito.verify(builder, never()).setEncryptionRequired();
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
-
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
-
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
-
- assertThat(entry, is(expectedEntry));
- }
-
- @Test
- @Config(sdk = 21)
- public void shouldCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI21() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21);
-
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
- KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
- PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
- //Set LockScreen as Enabled
- KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
- PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
- PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true);
- PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(), any())).thenReturn(new Intent());
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setSubject(principalCaptor.capture());
- Mockito.verify(builder).setAlias(KEY_ALIAS);
- Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setStartDate(startDateCaptor.capture());
- Mockito.verify(builder).setEndDate(endDateCaptor.capture());
- Mockito.verify(builder).setEncryptionRequired();
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
-
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
-
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
-
- assertThat(entry, is(expectedEntry));
- }
-
- @Test
- @Config(sdk = 23)
- public void shouldCreateRSAKeyPairIfMissingOnAPI23AndUp() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 23);
-
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
- KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
- PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
- Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
- Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
- Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
- Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
+ when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(keyStore);
+ when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)).thenReturn(keyPairGenerator);
+ when(KeyGenerator.getInstance(ALGORITHM_AES)).thenReturn(keyGenerator);
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
-
- assertThat(entry, is(expectedEntry));
- }
-
- @Test
- @Config(sdk = 28)
- public void shouldCreateRSAKeyPairIfMissingOnAPI28AndUp() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
-
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
- KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
- PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
- Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
- Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
- Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
- Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
-
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
-
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
+ PowerMockito.when(Cipher.getInstance(anyString())).thenAnswer((Answer) invocation -> {
+ String transformation = invocation.getArgument(0, String.class);
+ if (NEW_RSA_OAEP_TRANSFORMATION.equals(transformation)) {
+ return rsaOaepCipher;
+ } else if (OLD_RSA_PKCS1_TRANSFORMATION.equals(transformation)) {
+ return rsaPkcs1Cipher;
+ } else if (AES_TRANSFORMATION.equals(transformation)) {
+ return aesCipher;
+ }
+ Assert.fail("Cipher.getInstance called with unexpected transformation: " + transformation);
+ return null;
+ });
- assertThat(entry, is(expectedEntry));
+ cryptoUtil = PowerMockito.spy(new CryptoUtil(context, storage, BASE_ALIAS));
}
@Test
- @Config(sdk = 28)
- public void shouldCreateNewRSAKeyPairWhenExistingRSAKeyPairCannotBeRebuiltOnAPI28AndUp() throws Exception {
- ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
- PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
-
- //This is required to trigger the fallback when alias is present but key is not
- PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
- PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey).thenReturn(null);
- PowerMockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(null);
- //This is required to trigger finding the key after generating it
- KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);
-
- //Tests no instantiation of PrivateKeyEntry
- PowerMockito.verifyZeroInteractions(KeyStore.PrivateKeyEntry.class);
-
- //Creation assertion
- KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
- KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
- PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);
-
- ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
- ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class);
- ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class);
-
-
- final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();
-
- Mockito.verify(builder).setKeySize(2048);
- Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
- Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
- Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
- Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
- Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
- Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
- Mockito.verify(keyPairGenerator).initialize(spec);
- Mockito.verify(keyPairGenerator).generateKeyPair();
-
- assertThat(principalCaptor.getValue(), is(notNullValue()));
- assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));
-
- assertThat(startDateCaptor.getValue(), is(notNullValue()));
- long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
- long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(0L)); //Date is Today
-
- assertThat(endDateCaptor.getValue(), is(notNullValue()));
- diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
- days = TimeUnit.MILLISECONDS.toDays(diffMillis);
- assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days
-
- assertThat(entry, is(expectedEntry));
+ public void shouldThrowWhenRSAKeyAliasIsInvalid() {
+ Assert.assertThrows("RSA and AES Key alias must be valid.", IllegalArgumentException.class, () -> new CryptoUtil(context, storage, " "));
}
@Test
@Config(sdk = 28)
public void shouldUseExistingRSAKeyPairRebuildingTheEntryOnAPI28AndUp() throws Exception {
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
- KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
- PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
- Certificate certificate = PowerMockito.mock(Certificate.class);
-
- ArgumentCaptor