|
16 | 16 | import org.junit.Test; |
17 | 17 | import org.junit.runner.RunWith; |
18 | 18 | import org.mockito.ArgumentCaptor; |
| 19 | +import org.mockito.BDDMockito; |
19 | 20 | import org.mockito.Mockito; |
20 | 21 | import org.mockito.stubbing.Answer; |
21 | 22 | import org.powermock.api.mockito.PowerMockito; |
|
37 | 38 | import java.security.NoSuchProviderException; |
38 | 39 | import java.security.PrivateKey; |
39 | 40 | import java.security.ProviderException; |
| 41 | +import java.security.PublicKey; |
40 | 42 | import java.security.UnrecoverableEntryException; |
41 | 43 | import java.security.cert.Certificate; |
42 | 44 | import java.security.cert.CertificateException; |
|
63 | 65 | import static org.mockito.Matchers.anyInt; |
64 | 66 | import static org.mockito.Matchers.anyString; |
65 | 67 | import static org.mockito.Matchers.eq; |
| 68 | +import static org.mockito.Matchers.isNull; |
66 | 69 | import static org.mockito.Mockito.never; |
67 | 70 | import static org.mockito.Mockito.times; |
68 | 71 | import static org.mockito.Mockito.when; |
|
82 | 85 | * Read more: https://github.com/powermock/powermock/issues/992#issuecomment-662845804 |
83 | 86 | */ |
84 | 87 | @RunWith(PowerMockRunner.class) |
85 | | -@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class}) |
| 88 | +@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class, KeyStore.class}) |
86 | 89 | public class CryptoUtilTest { |
87 | 90 |
|
88 | 91 | private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; |
@@ -667,17 +670,19 @@ public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToObtainRSA |
667 | 670 | */ |
668 | 671 |
|
669 | 672 | @Test |
670 | | - public void shouldCreateAESKeyIfMissing() { |
| 673 | + public void shouldCreateAESKeyIfMissing() throws Exception { |
671 | 674 | byte[] sampleBytes = new byte[]{0, 1, 2, 3, 4, 5}; |
672 | 675 | PowerMockito.mockStatic(Base64.class); |
673 | 676 | PowerMockito.when(Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes()); |
674 | 677 | PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); |
| 678 | + PowerMockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); |
| 679 | + PowerMockito.mockStatic(TextUtils.class); |
| 680 | + PowerMockito.when(TextUtils.isEmpty(null)).thenReturn(true); |
675 | 681 |
|
676 | 682 | SecretKey secretKey = PowerMockito.mock(SecretKey.class); |
677 | 683 | PowerMockito.when(keyGenerator.generateKey()).thenReturn(secretKey); |
678 | 684 | PowerMockito.when(secretKey.getEncoded()).thenReturn(sampleBytes); |
679 | | - doReturn(sampleBytes).when(cryptoUtil).RSAEncrypt(sampleBytes); |
680 | | - |
| 685 | + PowerMockito.doReturn(sampleBytes).when(cryptoUtil, "RSAEncrypt", sampleBytes); |
681 | 686 |
|
682 | 687 | final byte[] aesKey = cryptoUtil.getAESKey(); |
683 | 688 |
|
@@ -740,9 +745,12 @@ public void shouldUseExistingAESKey() { |
740 | 745 | } |
741 | 746 |
|
742 | 747 | @Test |
743 | | - public void shouldThrowOnNoSuchAlgorithmExceptionWhenCreatingAESKey() { |
| 748 | + public void shouldThrowOnNoSuchAlgorithmExceptionWhenCreatingAESKey() throws Exception { |
744 | 749 | Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { |
745 | 750 | PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); |
| 751 | + PowerMockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); |
| 752 | + PowerMockito.mockStatic(TextUtils.class); |
| 753 | + PowerMockito.when(TextUtils.isEmpty(null)).thenReturn(true); |
746 | 754 | PowerMockito.mockStatic(KeyGenerator.class); |
747 | 755 | PowerMockito.when(KeyGenerator.getInstance(ALGORITHM_AES)) |
748 | 756 | .thenThrow(new NoSuchAlgorithmException()); |
@@ -1738,7 +1746,7 @@ public void shouldDetectAndMigratePKCS1KeyToOAEP() throws Exception { |
1738 | 1746 |
|
1739 | 1747 | Mockito.verify(storage).store(KEY_ALIAS, encodedEncryptedAESOAEP); |
1740 | 1748 |
|
1741 | | - Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); |
| 1749 | + // Note: We do NOT verify deleteEntry because RSA keys are preserved for backward compatibility |
1742 | 1750 | } |
1743 | 1751 |
|
1744 | 1752 | @Test |
@@ -1842,7 +1850,58 @@ public void shouldRecognizeIncompatiblePaddingModeInExceptionChain() throws Exce |
1842 | 1850 | byte[] result = cryptoUtil.getAESKey(); |
1843 | 1851 | assertThat(result, is(aesKeyBytes)); |
1844 | 1852 | Mockito.verify(rsaPkcs1Cipher).doFinal(encryptedAESBytes); |
1845 | | - Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); |
| 1853 | + |
| 1854 | + } |
| 1855 | + |
| 1856 | + @Test |
| 1857 | + public void shouldAllowMultipleRetrievalsAfterMigration() throws Exception { |
| 1858 | + |
| 1859 | + CryptoUtil cryptoUtil = newCryptoUtilSpy(); |
| 1860 | + |
| 1861 | + byte[] aesKeyBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; |
| 1862 | + byte[] encryptedAESKeyPKCS1 = new byte[]{20, 21, 22, 23, 24}; |
| 1863 | + String encodedEncryptedAESPKCS1 = "pkcs1_encrypted_key"; |
| 1864 | + |
| 1865 | + // First retrieval - migration happens |
| 1866 | + when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAESPKCS1); |
| 1867 | + when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(null); |
| 1868 | + PowerMockito.mockStatic(Base64.class); |
| 1869 | + PowerMockito.when(Base64.decode(encodedEncryptedAESPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESKeyPKCS1); |
| 1870 | + |
| 1871 | + IncompatibleDeviceException incompatibleException = new IncompatibleDeviceException( |
| 1872 | + new KeyStoreException("Incompatible padding mode") |
| 1873 | + ); |
| 1874 | + doThrow(incompatibleException).when(cryptoUtil).RSADecrypt(encryptedAESKeyPKCS1); |
| 1875 | + |
| 1876 | + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); |
| 1877 | + KeyStore.PrivateKeyEntry mockKeyEntry = mock(KeyStore.PrivateKeyEntry.class); |
| 1878 | + PrivateKey mockPrivateKey = mock(PrivateKey.class); |
| 1879 | + when(mockKeyEntry.getPrivateKey()).thenReturn(mockPrivateKey); |
| 1880 | + when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) |
| 1881 | + .thenReturn(mockKeyEntry); |
| 1882 | + |
| 1883 | + when(rsaPkcs1Cipher.doFinal(encryptedAESKeyPKCS1)).thenReturn(aesKeyBytes); |
| 1884 | + |
| 1885 | + byte[] encryptedAESKeyOAEP = new byte[]{30, 31, 32, 33, 34}; |
| 1886 | + doReturn(encryptedAESKeyOAEP).when(cryptoUtil).RSAEncrypt(aesKeyBytes); |
| 1887 | + String encodedEncryptedAESOAEP = "oaep_encrypted_key"; |
| 1888 | + PowerMockito.when(Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) |
| 1889 | + .thenReturn(encodedEncryptedAESOAEP.getBytes(StandardCharsets.UTF_8)); |
| 1890 | + |
| 1891 | + byte[] result1 = cryptoUtil.getAESKey(); |
| 1892 | + assertThat(result1, is(aesKeyBytes)); |
| 1893 | + Mockito.verify(storage).store(KEY_ALIAS, encodedEncryptedAESOAEP); |
| 1894 | + |
| 1895 | + when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAESOAEP); |
| 1896 | + PowerMockito.when(Base64.decode(encodedEncryptedAESOAEP, Base64.DEFAULT)).thenReturn(encryptedAESKeyOAEP); |
| 1897 | + |
| 1898 | + |
| 1899 | + doReturn(aesKeyBytes).when(cryptoUtil).RSADecrypt(encryptedAESKeyOAEP); |
| 1900 | + |
| 1901 | + byte[] result2 = cryptoUtil.getAESKey(); |
| 1902 | + assertThat(result2, is(aesKeyBytes)); |
| 1903 | + |
| 1904 | + Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS); |
1846 | 1905 | } |
1847 | 1906 |
|
1848 | 1907 | @Test |
|
0 commit comments