| /* |
| * Copyright 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.keystore.cts; |
| |
| import android.security.keystore.KeyProperties; |
| import android.security.keystore.KeyProtection; |
| import android.test.AndroidTestCase; |
| import android.test.MoreAsserts; |
| |
| import com.android.cts.keystore.R; |
| |
| import java.security.AlgorithmParameters; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.Provider; |
| import java.security.Security; |
| import java.security.Signature; |
| import java.security.SignatureException; |
| import java.security.spec.AlgorithmParameterSpec; |
| import java.security.spec.MGF1ParameterSpec; |
| import java.security.Provider.Service; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.spec.GCMParameterSpec; |
| import javax.crypto.spec.IvParameterSpec; |
| import javax.crypto.spec.OAEPParameterSpec; |
| import javax.crypto.spec.PSource; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| /** |
| * Tests for algorithm-agnostic functionality of {@code Cipher} implementations backed by Android |
| * Keystore. |
| */ |
| public class CipherTest extends AndroidTestCase { |
| |
| private static final String EXPECTED_PROVIDER_NAME = TestUtils.EXPECTED_CRYPTO_OP_PROVIDER_NAME; |
| |
| private static final String[] EXPECTED_ALGORITHMS = { |
| "AES/ECB/NoPadding", |
| "AES/ECB/PKCS7Padding", |
| "AES/CBC/NoPadding", |
| "AES/CBC/PKCS7Padding", |
| "AES/CTR/NoPadding", |
| "AES/GCM/NoPadding", |
| "RSA/ECB/NoPadding", |
| "RSA/ECB/PKCS1Padding", |
| "RSA/ECB/OAEPPadding", |
| "RSA/ECB/OAEPWithSHA-1AndMGF1Padding", |
| "RSA/ECB/OAEPWithSHA-224AndMGF1Padding", |
| "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", |
| "RSA/ECB/OAEPWithSHA-384AndMGF1Padding", |
| "RSA/ECB/OAEPWithSHA-512AndMGF1Padding", |
| }; |
| |
| private static class KatVector { |
| private final byte[] plaintext; |
| private final byte[] ciphertext; |
| private final AlgorithmParameterSpec params; |
| |
| private KatVector(String plaintextHex, String ciphertextHex) { |
| this(plaintextHex, null, ciphertextHex); |
| } |
| |
| private KatVector(String plaintextHex, AlgorithmParameterSpec params, |
| String ciphertextHex) { |
| this(HexEncoding.decode(plaintextHex), params, HexEncoding.decode(ciphertextHex)); |
| } |
| |
| private KatVector(byte[] plaintext, byte[] ciphertext) { |
| this(plaintext, null, ciphertext); |
| } |
| |
| private KatVector(byte[] plaintext, AlgorithmParameterSpec params, byte[] ciphertext) { |
| this.plaintext = plaintext; |
| this.ciphertext = ciphertext; |
| this.params = params; |
| } |
| } |
| private static final Map<String, KatVector> KAT_VECTORS = |
| new TreeMap<String, KatVector>(String.CASE_INSENSITIVE_ORDER); |
| static { |
| // From RI |
| KAT_VECTORS.put("AES/ECB/NoPadding", new KatVector( |
| "0383911bb1519d58e6656f3fd35639c502dbeb2196cea937fca272666cb4a80b", |
| "6574c5065283b89e0c930019e4655d8516b98170db6516cd83e589bd9c5e5adc")); |
| KAT_VECTORS.put("AES/ECB/PKCS7Padding", new KatVector( |
| "1ad3d73a3cfa66dac78a51a95c2cb2125ea701e6e9ecbca2415b436f0258e2ba7439b67545", |
| "920f873f2f9e91bac4c9c948d66496a21b8b2606850490dac7abecae83317488ee550b9973ac5cd142" |
| + "f387d7d2a12752")); |
| KAT_VECTORS.put("AES/CBC/NoPadding", new KatVector( |
| "1dffe21c8f18276c3a39ed0c53ab257b84efcedab60095c4cadd131143058cf7", |
| new IvParameterSpec(HexEncoding.decode("10b3eea6cc8a7d6f48337e9b6987d28c")), |
| "47ab115bfadca91eaebec73ab942a06f3121fdd5aa55d223bd2cbcc3855e1ef8")); |
| KAT_VECTORS.put("AES/CBC/PKCS7Padding", new KatVector( |
| "9d49fb970b23bfe742ae7c45a773ada9faad84708c8858a06e4a192e0a90e2f6083548e0bf3f67", |
| new IvParameterSpec(HexEncoding.decode("ecd87bf9c49f37dcd2294e309192289a")), |
| "aeb64f48ec18a086eda7ee080948651a50b6f582ab54aac5454c9ab0a4de5b4a4abac526a4307011d1" |
| + "2881f1849c32ae")); |
| KAT_VECTORS.put("AES/CTR/NoPadding", new KatVector( |
| "b4e786cab9df48d2fce0c7872651314db1318d1f31a1b10a2c334d2555b4117668", |
| new IvParameterSpec(HexEncoding.decode("94d9f7a6d16f58018819b668020b68cc")), |
| "022e74572a70be57a0b65b2fb5bc9b803ce48973b6163f528bbe1fd001e29d330a")); |
| KAT_VECTORS.put("AES/GCM/NoPadding", new KatVector( |
| "03889a6ca811e3fd7e78467e3dae587d2110e80e98edbc9dfe17afba238c4c493186", |
| new GCMParameterSpec(128, HexEncoding.decode("f67aaf97cdec65b12188315e")), |
| "159eb1ffc86589b38f18097c32db646c7de3525b603876c3ae671bc2ca52a5395a374b377a915c9ed1" |
| + "a349abf9fc54c9ca81")); |
| KAT_VECTORS.put("RSA/ECB/NoPadding", new KatVector( |
| "50c499d558c38fd48ea76832887db2abc76e4e153a98fd4323ccb8006d34f11724a5692fb101b0eb96" |
| + "060eb9d15222", |
| "349b1d5061e98d0ab3f2327680bbc0cbb1b8ef8ee26148d7c67cf535223e3f78d822d369592ede29b1" |
| + "654aab25e6ae5e098318e55c13dc405f5ba27e5cc69ced32778592a51e6293a03f95e14ed17099fb" |
| + "0ac585e41297b87c3432953df0d98be7e505dc7de7bfe9d9ec750f475afeba4cc2dd78838c0d4399" |
| + "d8de02b07f00b292dc3d32d2a2f98ea5a5dac1a0fec4d01e5c3aea8c56eeff264896fb6cf2144401" |
| + "278c6663417bc00aafbb9eb97c056573cdec88d6ac6fd6c333d131337b16031da229029e3b6fe6f8" |
| + "ee427f2e90041e9636d67cddac75845914ce4be56092eed7188fe7e2bb33769efdeed86a7acbe15d" |
| + "debf92d9fbaaddede206acfa650697")); |
| KAT_VECTORS.put("RSA/ECB/PKCS1Padding", new KatVector( |
| "aed8cd94f35b2a54cdd3ed771482bd87e256b995408558fb82e5d475d1ee54711472f899ad6cbb6847" |
| + "99e52ff1d57cbc39f4", |
| "64148dee294dd3ea31d2b595ea661318cf90c89f71393cf6559087d6e8993e73eb1e6b5f4d3cfde3cb" |
| + "267938c5eca522b95a2df02df9c703dbe3103c157af0d2ed5b70da51cb4caa49061319420d0ea433" |
| + "f24b727530c162226bc806b7f39079cd494a5c8a242737413d27063f9fb74aadd20f521211316719" |
| + "c628fd4351d0608928949b6f59f351d9ccec4c596514335010834fcabd53a2cbb2642e0f83c4f89c" |
| + "199ee2c68ace9182cf484d99e86b0b2213c1cc113d24891958e5a0774b7486abae1475e46a939a94" |
| + "5d6491b98ad7979fd6e752b47e43e960557a0c0589d7d0444b011d75c9f5b143da6e1dcf7b678a2e" |
| + "f82fbe37a74df3e20fb1a9dbfd5978")); |
| KAT_VECTORS.put("RSA/ECB/OAEPPadding", new KatVector( |
| "c219f4e3e37eae2315f0fa4ebc4b46ef0c6befbb43a51ceda07435fc88a9", |
| "7a9bcfd0d02b6434025bbf5ba09c2dad118a4a3bca7cced8b404bc0fc2f17ddee13de82c8324294bf2" |
| + "60ad6e5171c2c3728a0c0fab20dd60e4e56cfef3e66239439ed2eddcc83ac8eeaedfd970e9966de3" |
| + "94ad1df0df503a0a640a49e10885b3a4115c3e94e893fff87bf9a5808350f957d6bc556ca6b08f81" |
| + "bf697704a3eb3db774797f883af0dcdc9bd9196d7595bab5e87d3187eb45b5771abe4e4dc70c25fa" |
| + "b9e3cddb6ae453a1d8e517d000779472e1376e5848b1654a51a9e90be4a4a6d0f6b8723c6e93c471" |
| + "313ea94f24504ca377b502057331355965a7e0b9c3b1d1fbd24ab5a4167f721d1ddac4d3c094d5c9" |
| + "0d2e277e9b5617cbf2770186323e89")); |
| KAT_VECTORS.put("RSA/ECB/OAEPWithSHA-1AndMGF1Padding", new KatVector( |
| "bb2854620bb0e361d1384703dda12acee1fefc22024bcfc40a86390d5342c693aab8c7ed6517d8da86" |
| + "04492c9d", |
| "77033c578f24ef0ed93bfe6dc6f7c3f9f0505e7562f67ce987a269cabaa8a3ae7dd5e567a8b37db42d" |
| + "a79aa86ea2e189af5b9560b39407ff86f2785cdaf660fc7c93649bc24a818de564cb0d03e7681fa8" |
| + "f3cd42b3bfc58c49d3f049e0c98b07aff95876f05ddc45ebaa7127a198f27ae0cfd161c5598ac795" |
| + "8ed386d98b13d45730e6dc16313fe012af27d7be0e45215040bbfb07f2d35e34291fe4335a68175a" |
| + "46be99a15c1ccf673659157e1f52105de5a0a6f8c9d946740216eefe2a01a37b0ab144a44ff0d800" |
| + "be713b5b44acf4fcb1a60d5db977af4d77fa77bdb8594032b2f5bbdd49346b08e0e98ab1051b462e" |
| + "160c1bff62b927cd26c936948b723a")); |
| KAT_VECTORS.put("RSA/ECB/OAEPWithSHA-224AndMGF1Padding", new KatVector( |
| "1bae19434be6599d1987b1ed866dd6b684dcd908bd98d797250be545eafea46d05ebdf9018", |
| "0f18b4a1153c6f8821e18a4275e4b570d540c8ad86bfc99146e5475238a43ecbe63bc81368cd64b9a2" |
| + "ab3ccd586e6afaad054c9d7bdc986adf022ec86335d110c53ebd5f2f2bd49d48d6da9541312c9b1b" |
| + "cc299ca4f59475869e4ec2253c91b137eae274a245fc9ee6262f74754bbda55d8bd25bfa4c1698f3" |
| + "a22d2d8d7fc6e9fbb56d828e61912b3085d82cceaeb1d2da425871575e7ba31a3d47b1b7d7df0bda" |
| + "81d62c75a9887bbc528fc6bb51db09884bb513b4cc94ca4a5fe0b370ca548dcdf60eebbf61e7efe7" |
| + "630fc47256d6d617fc1c2c774405f385650898abea03502cfbdcb53579fd18d896490e67aecdb7c7" |
| + "b7b950dc7ddba5c64188494c1a177b")); |
| KAT_VECTORS.put("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", new KatVector( |
| "332c2f2fc066fb29ec0928a52b5111ce6965546ce73927340c42d33b56b6ba547b77ac361ac0d13316" |
| + "345ca953840023d892fa4ff1aa32cc66d5aa88b79867", |
| "942c0ba1c67a34a7e116d9281b1df5084c66bc1458faf1b26d4f0f63a57307a9addcd3e5d2f3320071" |
| + "5a3d95ae84fb40a8dfe4cb0a28873fd5883ff8ee6efbfe38c460c755577b34fcf05bb2077afec7b2" |
| + "203799022be6a0903915e01e94abc51efe9c5548eb86bbbb4fd7f3bfc7b86f388128b6df1e6ce651" |
| + "230c6bc18bbf55b029f1e31da880c27d947ff97519df66a57ead6db791c4978f1d62edec0d89bb16" |
| + "83d237213f3f24271ddb8c4b50a82527954f0e49ae44d3acd8ddd3a57cfbfa456dd40675d5d75542" |
| + "31c6b79c7fb3500b1631be1d100e67d85ce423845fdc7c7f45e346a8ba573f5d11de9009069468dd" |
| + "8d517ad4adb1509dd5173ee1862d74")); |
| KAT_VECTORS.put("RSA/ECB/OAEPWithSHA-384AndMGF1Padding", new KatVector( |
| "f51f158cbad4dbab38403b839c724f09a480c49be29c0e72615539dbe57ec86143f31f19392f419b5b" |
| + "e4ba9e3c6f1e870d307a7cf1a9e2", |
| "944243f35f534e7a273e94986b6835a4f5cdc5bc4efb9970d4760986599a02f652a848fcae333ff25a" |
| + "64108c9b900aaf002688398ad9fc17c73be52726306af9c13540df9d1765336b6f09ba4cb8a54d72" |
| + "5a4e45854bfa3802cfb110a6d7f7054e6072440ec00da62828cb75fe2566ec5be79eb8a3d1fbe2c2" |
| + "4439c107e5018e445e201ad80725755543c00dec50bb464c6ca897600eb3cda51fcef8161ac13d75" |
| + "a3eb30d385a1e718a61ae1b5d47aadb966fc007becc84db397d0b3cd983121872f9975995153e869" |
| + "9e24554a3c5e885f0ed8cd03e916da5ed541f1598da9bd6209447301d00f086153da353deff9d045" |
| + "8976ff7570410f0bdcfb3f56b782f5")); |
| KAT_VECTORS.put("RSA/ECB/OAEPWithSHA-512AndMGF1Padding", new KatVector( |
| "d45f6ccc7e663957f234c237c1f09bf7791f6f5c1b9ef4fefb16e55ded0d96112e590f1bb08a60f85c" |
| + "2d0d2533f1d69792dfd8d647d880b18f87cfe32488c73613a3d535da7d776d90d9a4ba6a0311f456" |
| + "8511da49107c", |
| "5a037df3e5d6f3f703541e2db2aef7c69985e513bdff67c8ade6a09f50e27267bfb444f6c69b40a77a" |
| + "9136a27b29876af9d2bf4e7099863445d35b188d31f376b89fbd196059667ca657e10b9454c2b25f" |
| + "046fc9f7b42506e382e6b6fd99409cf97e865e65f8dce5d14a06b8aa8833c4bc72c8764467758f2d" |
| + "7960243161dce4ca8231e91bfcd3c933a80bc703ceab976224c876b1f550f91a6c2a0332d4377bd8" |
| + "dfe4b1283ab114e517b7b9e4a6e0bf166d5b506e7a3b7328078e12cb23b1d938760767dc9b3c3eb0" |
| + "848ddda101792aca9273ad414314c13fc511ffa0358a8f4c5f38edded3a2dc111fa62c80e6032c32" |
| + "ae04aeac7729f16a6310f1f6785c27")); |
| } |
| |
| private static final long DAY_IN_MILLIS = TestUtils.DAY_IN_MILLIS; |
| |
| private static final byte[] AES128_KAT_KEY_BYTES = |
| HexEncoding.decode("7d9f11a0da111e9d8bdd14f04648ed91"); |
| |
| private static final byte[] AES192_KAT_KEY_BYTES = |
| HexEncoding.decode("69ef2c44a48d3dc4d5744a281f7ebb5ca976c2202f91e10c"); |
| |
| private static final byte[] AES256_KAT_KEY_BYTES = |
| HexEncoding.decode("cf601cc10aaf434d1f01747136aff222af7fb426d101901712214c3fea18125f"); |
| |
| private static final KeyProtection.Builder GOOD_IMPORT_PARAMS_BUILDER = |
| new KeyProtection.Builder( |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) |
| .setBlockModes(KeyProperties.BLOCK_MODE_ECB, |
| KeyProperties.BLOCK_MODE_CBC, |
| KeyProperties.BLOCK_MODE_CTR, |
| KeyProperties.BLOCK_MODE_GCM) |
| .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) |
| .setDigests(KeyProperties.DIGEST_NONE) |
| .setRandomizedEncryptionRequired(false); |
| |
| public void testAlgorithmList() { |
| // Assert that Android Keystore Provider exposes exactly the expected Cipher |
| // transformations. We don't care whether the transformations are exposed via aliases, as |
| // long as canonical names of transformation are accepted. |
| // If the Provider exposes extraneous algorithms, it'll be caught because it'll have to |
| // expose at least one Service for such an algorithm, and this Service's algorithm will |
| // not be in the expected set. |
| |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| Set<Service> services = provider.getServices(); |
| Set<String> actualAlgsLowerCase = new HashSet<String>(); |
| Set<String> expectedAlgsLowerCase = new HashSet<String>( |
| Arrays.asList(TestUtils.toLowerCase(EXPECTED_ALGORITHMS))); |
| for (Service service : services) { |
| if ("Cipher".equalsIgnoreCase(service.getType())) { |
| String algLowerCase = service.getAlgorithm().toLowerCase(Locale.US); |
| actualAlgsLowerCase.add(algLowerCase); |
| } |
| } |
| |
| TestUtils.assertContentsInAnyOrder(actualAlgsLowerCase, |
| expectedAlgsLowerCase.toArray(new String[0])); |
| } |
| |
| public void testAndroidKeyStoreKeysHandledByAndroidKeyStoreProviderWhenDecrypting() |
| throws Exception { |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| try { |
| ImportedKey key = importDefaultKatKey( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| false); |
| |
| // Decryption may need additional parameters. Initializing a Cipher for encryption |
| // forces it to generate any such parameters. |
| Cipher cipher = Cipher.getInstance(algorithm, provider); |
| cipher.init(Cipher.ENCRYPT_MODE, key.getKeystoreBackedEncryptionKey()); |
| AlgorithmParameters params = cipher.getParameters(); |
| |
| // Test DECRYPT_MODE |
| cipher = Cipher.getInstance(algorithm); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| assertSame(provider, cipher.getProvider()); |
| |
| // Test UNWRAP_MODE |
| cipher = Cipher.getInstance(algorithm); |
| if (params != null) { |
| cipher.init(Cipher.UNWRAP_MODE, decryptionKey, params); |
| } else { |
| cipher.init(Cipher.UNWRAP_MODE, decryptionKey); |
| } |
| assertSame(provider, cipher.getProvider()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + algorithm, e); |
| } |
| } |
| } |
| |
| public void testAndroidKeyStorePublicKeysAcceptedByHighestPriorityProviderWhenEncrypting() |
| throws Exception { |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(algorithm)) { |
| continue; |
| } |
| try { |
| Key key = importDefaultKatKey( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT, |
| false).getKeystoreBackedEncryptionKey(); |
| |
| Cipher cipher = Cipher.getInstance(algorithm); |
| cipher.init(Cipher.ENCRYPT_MODE, key); |
| |
| cipher = Cipher.getInstance(algorithm); |
| cipher.init(Cipher.WRAP_MODE, key); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for" + algorithm, e); |
| } |
| } |
| } |
| |
| public void testEmptyPlaintextEncryptsAndDecrypts() |
| throws Exception { |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| final byte[] originalPlaintext = EmptyArray.BYTE; |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| false)) { |
| try { |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| byte[] plaintext = truncatePlaintextIfNecessary( |
| algorithm, encryptionKey, originalPlaintext); |
| if (plaintext == null) { |
| // Key is too short to encrypt anything using this transformation |
| continue; |
| } |
| Cipher cipher = Cipher.getInstance(algorithm, provider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| AlgorithmParameters params = cipher.getParameters(); |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| byte[] expectedPlaintext = plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL |
| // bytes to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| |
| cipher = Cipher.getInstance(algorithm, provider); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| byte[] actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias(), |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testCiphertextGeneratedByAndroidKeyStoreDecryptsByAndroidKeyStore() |
| throws Exception { |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| final byte[] originalPlaintext = "Very secret message goes here...".getBytes("US-ASCII"); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| false)) { |
| try { |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| byte[] plaintext = truncatePlaintextIfNecessary( |
| algorithm, encryptionKey, originalPlaintext); |
| if (plaintext == null) { |
| // Key is too short to encrypt anything using this transformation |
| continue; |
| } |
| Cipher cipher = Cipher.getInstance(algorithm, provider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| AlgorithmParameters params = cipher.getParameters(); |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| byte[] expectedPlaintext = plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL |
| // bytes to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| |
| cipher = Cipher.getInstance(algorithm, provider); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| byte[] actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias(), |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testCiphertextGeneratedByHighestPriorityProviderDecryptsByAndroidKeyStore() |
| throws Exception { |
| Provider keystoreProvider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(keystoreProvider); |
| byte[] originalPlaintext = "Very secret message goes here...".getBytes("UTF-8"); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_DECRYPT, |
| false)) { |
| Provider encryptionProvider = null; |
| try { |
| Key encryptionKey = key.getOriginalEncryptionKey(); |
| byte[] plaintext = truncatePlaintextIfNecessary( |
| algorithm, encryptionKey, originalPlaintext); |
| if (plaintext == null) { |
| // Key is too short to encrypt anything using this transformation |
| continue; |
| } |
| |
| Cipher cipher; |
| try { |
| cipher = Cipher.getInstance(algorithm); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| } catch (InvalidKeyException e) { |
| // No providers support encrypting using this algorithm and key. |
| continue; |
| } |
| encryptionProvider = cipher.getProvider(); |
| if (keystoreProvider == encryptionProvider) { |
| // This is covered by another test. |
| continue; |
| } |
| AlgorithmParameters params = cipher.getParameters(); |
| |
| // TODO: Remove this workaround for Bug 22405492 once the issue is fixed. The |
| // issue is that Bouncy Castle incorrectly defaults the MGF1 digest to the |
| // digest specified in the transformation. RI and Android Keystore keep the MGF1 |
| // digest defaulted at SHA-1. |
| if ((params != null) && ("OAEP".equalsIgnoreCase(params.getAlgorithm()))) { |
| OAEPParameterSpec spec = params.getParameterSpec(OAEPParameterSpec.class); |
| if (!"SHA-1".equalsIgnoreCase( |
| ((MGF1ParameterSpec) spec.getMGFParameters()) |
| .getDigestAlgorithm())) { |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new OAEPParameterSpec( |
| spec.getDigestAlgorithm(), |
| "MGF1", |
| MGF1ParameterSpec.SHA1, |
| PSource.PSpecified.DEFAULT)); |
| params = cipher.getParameters(); |
| } |
| } |
| |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| byte[] expectedPlaintext = plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL |
| // bytes to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| |
| // TODO: Remove this workaround for Bug 22319986 once the issue is fixed. The issue |
| // is that Conscrypt and Bouncy Castle's AES/GCM/NoPadding implementations return |
| // AlgorithmParameters of algorithm "AES" from which it's impossible to obtain a |
| // GCMParameterSpec. They should be returning AlgorithmParameters of algorithm |
| // "GCM". |
| if (("AES/GCM/NoPadding".equalsIgnoreCase(algorithm)) |
| && (!"GCM".equalsIgnoreCase(params.getAlgorithm()))) { |
| params = AlgorithmParameters.getInstance("GCM"); |
| params.init(new GCMParameterSpec(128, cipher.getIV())); |
| } |
| |
| cipher = Cipher.getInstance(algorithm, keystoreProvider); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| byte[] actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias() |
| + ", encryption provider: " + encryptionProvider, |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testCiphertextGeneratedByAndroidKeyStoreDecryptsByHighestPriorityProvider() |
| throws Exception { |
| Provider keystoreProvider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(keystoreProvider); |
| byte[] originalPlaintext = "Very secret message goes here...".getBytes("UTF-8"); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT, |
| false)) { |
| Provider decryptionProvider = null; |
| try { |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| byte[] plaintext = truncatePlaintextIfNecessary( |
| algorithm, encryptionKey, originalPlaintext); |
| if (plaintext == null) { |
| // Key is too short to encrypt anything using this transformation |
| continue; |
| } |
| Cipher cipher = Cipher.getInstance(algorithm, keystoreProvider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| AlgorithmParameters params = cipher.getParameters(); |
| |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| byte[] expectedPlaintext = plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL |
| // bytes to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| |
| Key decryptionKey = key.getOriginalDecryptionKey(); |
| try { |
| cipher = Cipher.getInstance(algorithm); |
| if (params != null) { |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| } else { |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey); |
| } |
| } catch (InvalidKeyException e) { |
| // No providers support decrypting using this algorithm and key. |
| continue; |
| } |
| decryptionProvider = cipher.getProvider(); |
| if (keystoreProvider == decryptionProvider) { |
| // This is covered by another test. |
| continue; |
| } |
| byte[] actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias() |
| + ", decryption provider: " + decryptionProvider, |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testMaxSizedPlaintextSupported() throws Exception { |
| Provider keystoreProvider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(keystoreProvider); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(algorithm)) { |
| // No input length restrictions (except multiple of block size for some |
| // transformations). |
| continue; |
| } |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| false)) { |
| int plaintextSizeBytes = -1; |
| Provider otherProvider = null; |
| try { |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| int maxSupportedPlaintextSizeBytes = |
| TestUtils.getMaxSupportedPlaintextInputSizeBytes( |
| algorithm, encryptionKey); |
| if (maxSupportedPlaintextSizeBytes < 0) { |
| // Key too short to encrypt anything using this transformation. |
| continue; |
| } else if (maxSupportedPlaintextSizeBytes == Integer.MAX_VALUE) { |
| // No input length restrictions. |
| continue; |
| } |
| byte[] plaintext = new byte[maxSupportedPlaintextSizeBytes]; |
| Arrays.fill(plaintext, (byte) 0xff); |
| plaintextSizeBytes = plaintext.length; |
| |
| // Encrypt plaintext using Android Keystore Cipher |
| Cipher cipher = Cipher.getInstance(algorithm, keystoreProvider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| AlgorithmParameters params = cipher.getParameters(); |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| byte[] expectedPlaintext = plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL |
| // bytes to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| |
| // Check that ciphertext decrypts using Android Keystore Cipher |
| cipher = Cipher.getInstance(algorithm, keystoreProvider); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| byte[] actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| |
| // Check that ciphertext decrypts using the highest-priority provider. |
| cipher = Cipher.getInstance(algorithm); |
| decryptionKey = key.getOriginalDecryptionKey(); |
| try { |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params); |
| } catch (InvalidKeyException e) { |
| // No other providers offer decryption using this transformation and key. |
| continue; |
| } |
| otherProvider = cipher.getProvider(); |
| if (otherProvider == keystoreProvider) { |
| // This has already been tested above. |
| continue; |
| } |
| actualPlaintext = cipher.doFinal(ciphertext); |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias() |
| + " and " + plaintextSizeBytes + " long plaintext" |
| + ", other provider: " + otherProvider, |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testLargerThanMaxSizedPlaintextRejected() throws Exception { |
| Provider keystoreProvider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(keystoreProvider); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(algorithm)) { |
| // No input length restrictions (except multiple of block size for some |
| // transformations). |
| continue; |
| } |
| for (ImportedKey key : importKatKeys( |
| algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| false)) { |
| int plaintextSizeBytes = -1; |
| Provider otherProvider = null; |
| try { |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| int maxSupportedPlaintextSizeBytes = |
| TestUtils.getMaxSupportedPlaintextInputSizeBytes( |
| algorithm, encryptionKey); |
| if (maxSupportedPlaintextSizeBytes < 0) { |
| // Key too short to encrypt anything using this transformation. |
| continue; |
| } else if (maxSupportedPlaintextSizeBytes == Integer.MAX_VALUE) { |
| // No input length restrictions. |
| continue; |
| } |
| // Create plaintext which is one byte longer than maximum supported one. |
| byte[] plaintext = new byte[maxSupportedPlaintextSizeBytes + 1]; |
| Arrays.fill(plaintext, (byte) 0xff); |
| plaintextSizeBytes = plaintext.length; |
| |
| // Encrypting this plaintext using Android Keystore Cipher should fail. |
| Cipher cipher = Cipher.getInstance(algorithm, keystoreProvider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // This transformation is special: it's supposed to throw a |
| // BadPaddingException instead of an IllegalBlockSizeException. |
| try { |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| fail("Unexpectedly produced ciphertext (" + ciphertext.length |
| + " bytes): " + HexEncoding.encode(ciphertext) + " for " |
| + plaintext.length + " byte long plaintext"); |
| } catch (BadPaddingException expected) {} |
| } else { |
| try { |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| fail("Unexpectedly produced ciphertext (" + ciphertext.length |
| + " bytes): " + HexEncoding.encode(ciphertext) + " for " |
| + plaintext.length + " byte long plaintext"); |
| } catch (IllegalBlockSizeException expected) {} |
| } |
| |
| // Encrypting this plaintext using the highest-priority implementation should |
| // fail. |
| cipher = Cipher.getInstance(algorithm); |
| encryptionKey = key.getOriginalEncryptionKey(); |
| try { |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); |
| } catch (InvalidKeyException e) { |
| // No other providers support this transformation with this key. |
| continue; |
| } |
| otherProvider = cipher.getProvider(); |
| if (otherProvider == keystoreProvider) { |
| // This has already been tested above. |
| continue; |
| } |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // This transformation is special: it's supposed to throw a |
| // BadPaddingException instead of an IllegalBlockSizeException. |
| try { |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| fail(otherProvider.getName() + " unexpectedly produced ciphertext (" |
| + ciphertext.length + " bytes): " |
| + HexEncoding.encode(ciphertext) + " for " |
| + plaintext.length + " byte long plaintext"); |
| // TODO: Remove this workaround once Conscrypt's RSA Cipher Bug 22567458 |
| // is fixed. Conscrypt's Cipher.doFinal throws a SignatureException. |
| // This code is unreachable because of the fail() above. It's here only |
| // so that the compiler does not complain about us catching |
| // SignatureException. |
| Signature sig = Signature.getInstance("SHA256withRSA"); |
| sig.sign(); |
| } catch (BadPaddingException | SignatureException expected) {} |
| } else { |
| try { |
| byte[] ciphertext = cipher.doFinal(plaintext); |
| fail(otherProvider.getName() + " unexpectedly produced ciphertext (" |
| + ciphertext.length + " bytes): " |
| + HexEncoding.encode(ciphertext) + " for " |
| + plaintext.length + " byte long plaintext"); |
| // TODO: Remove the catching of RuntimeException workaround once the |
| // corresponding Bug 22567463 in Conscrypt is fixed. |
| } catch (IllegalBlockSizeException | RuntimeException expected) {} |
| } |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + algorithm + " with key " + key.getAlias() |
| + " and " + plaintextSizeBytes + " byte long plaintext" |
| + ", other provider: " + otherProvider, |
| e); |
| } |
| } |
| } |
| } |
| |
| public void testKat() throws Exception { |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| for (String algorithm : EXPECTED_ALGORITHMS) { |
| try { |
| ImportedKey key = importDefaultKatKey(algorithm, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| true); |
| KatVector testVector = KAT_VECTORS.get(algorithm); |
| assertNotNull(testVector); |
| Cipher cipher = Cipher.getInstance(algorithm, provider); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, testVector.params); |
| byte[] actualPlaintext = cipher.doFinal(testVector.ciphertext); |
| byte[] expectedPlaintext = testVector.plaintext; |
| if ("RSA/ECB/NoPadding".equalsIgnoreCase(algorithm)) { |
| // RSA decryption without padding left-pads resulting plaintext with NUL bytes |
| // to the length of RSA modulus. |
| int modulusLengthBytes = (TestUtils.getKeySizeBits(decryptionKey) + 7) / 8; |
| expectedPlaintext = TestUtils.leftPadWithZeroBytes( |
| expectedPlaintext, modulusLengthBytes); |
| } |
| MoreAsserts.assertEquals(expectedPlaintext, actualPlaintext); |
| if (!isRandomizedEncryption(algorithm)) { |
| // Deterministic encryption: ciphertext depends only on plaintext and input |
| // parameters. Assert that encrypting the plaintext results in the same |
| // ciphertext as in the test vector. |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| cipher = Cipher.getInstance(algorithm, provider); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, testVector.params); |
| byte[] actualCiphertext = cipher.doFinal(testVector.plaintext); |
| MoreAsserts.assertEquals(testVector.ciphertext, actualCiphertext); |
| } |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + algorithm, e); |
| } |
| } |
| } |
| |
| private static boolean isRandomizedEncryption(String transformation) { |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| return (transformationUpperCase.endsWith("/PKCS1PADDING")) |
| || (transformationUpperCase.contains("OAEP")); |
| } |
| |
| public void testInitDecryptFailsWhenNotAuthorizedToDecrypt() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good, KeyProperties.PURPOSE_ENCRYPT).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenNotAuthorizedToEncrypt() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good, KeyProperties.PURPOSE_DECRYPT).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresAuthorizedPurposes() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good, 0).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptFailsWhenBlockModeNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| if (transformationUpperCase.startsWith("RSA/")) { |
| // Block modes do not apply |
| continue; |
| } |
| String authorizedBlockMode = |
| (transformationUpperCase.contains("/CBC/")) ? "CTR" : "CBC"; |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setBlockModes(authorizedBlockMode).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedBlockMode, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenBlockModeNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| if (transformationUpperCase.startsWith("RSA/")) { |
| // Block modes do not apply |
| continue; |
| } |
| String authorizedBlockMode = |
| (transformationUpperCase.contains("/CBC/")) ? "CTR" : "CBC"; |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setBlockModes(authorizedBlockMode).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedBlockMode, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresAuthorizedBlockModes() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good).setBlockModes().build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptFailsWhenDigestNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| if ((transformationUpperCase.endsWith("/NOPADDING")) |
| || (transformationUpperCase.endsWith("/PKCS1PADDING")) |
| || (transformationUpperCase.endsWith("/PKCS7PADDING"))) { |
| // Digest not used |
| continue; |
| } |
| String authorizedDigest = |
| (transformationUpperCase.contains("SHA-256")) ? "SHA-512" : "SHA-256"; |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setDigests(authorizedDigest).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedDigest, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenDigestNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| if ((transformationUpperCase.endsWith("/NOPADDING")) |
| || (transformationUpperCase.endsWith("/PKCS1PADDING")) |
| || (transformationUpperCase.endsWith("/PKCS7PADDING"))) { |
| // Digest not used |
| continue; |
| } |
| String authorizedDigest = |
| (transformationUpperCase.contains("SHA-256")) ? "SHA-512" : "SHA-256"; |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setDigests(authorizedDigest).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedDigest, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresAuthorizedDigests() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good).setDigests().build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptFailsWhenPaddingSchemeNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| String authorizedPaddingScheme; |
| if (transformationUpperCase.startsWith("RSA/")) { |
| authorizedPaddingScheme = transformationUpperCase.contains("PKCS1PADDING") |
| ? "OAEPPadding" : "PKCS1Padding"; |
| } else { |
| authorizedPaddingScheme = transformationUpperCase.contains("PKCS1PADDING") |
| ? "NoPadding" : "PKCS1Padding"; |
| } |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good) |
| .setEncryptionPaddings(authorizedPaddingScheme) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedPaddingScheme, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenPaddingSchemeNotAuthorized() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| String transformationUpperCase = transformation.toUpperCase(Locale.US); |
| String authorizedPaddingScheme; |
| if (transformationUpperCase.startsWith("RSA/")) { |
| authorizedPaddingScheme = transformationUpperCase.contains("PKCS1PADDING") |
| ? "OAEPPadding" : "PKCS1Padding"; |
| } else { |
| authorizedPaddingScheme = transformationUpperCase.contains("PKCS1PADDING") |
| ? "NoPadding" : "PKCS1Padding"; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good) |
| .setEncryptionPaddings(authorizedPaddingScheme) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " when authorized only for " |
| + authorizedPaddingScheme, |
| e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresAuthorizedPaddingSchemes() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good) |
| .setEncryptionPaddings() |
| .setSignaturePaddings() |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptFailsWhenKeyNotYetValid() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badStartDate = new Date(System.currentTimeMillis() + DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setKeyValidityStart(badStartDate).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenKeyNotYetValid() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badStartDate = new Date(System.currentTimeMillis() + DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good).setKeyValidityStart(badStartDate).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresThatKeyNotYetValid() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badStartDate = new Date(System.currentTimeMillis() + DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good).setKeyValidityStart(badStartDate).build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptFailsWhenKeyNoLongerValidForConsumption() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badEndDate = new Date(System.currentTimeMillis() - DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForConsumptionEnd(badEndDate) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitDecryptIgnoresThatKeyNoLongerValidForOrigination() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badEndDate = new Date(System.currentTimeMillis() - DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| try { |
| assertInitDecryptSucceeds(transformation, good.build()); |
| assertInitDecryptSucceeds(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForOriginationEnd(badEndDate) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricFailsWhenKeyNoLongerValidForOrigination() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badEndDate = new Date(System.currentTimeMillis() - DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptThrowsInvalidKeyException(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForOriginationEnd(badEndDate) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptSymmetricIgnoresThatKeyNoLongerValidForConsumption() |
| throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badEndDate = new Date(System.currentTimeMillis() - DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (!isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForConsumptionEnd(badEndDate) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testInitEncryptAsymmetricIgnoresThatKeyNoLongerValid() throws Exception { |
| KeyProtection.Builder good = GOOD_IMPORT_PARAMS_BUILDER; |
| Date badEndDate = new Date(System.currentTimeMillis() - DAY_IN_MILLIS); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| if (isSymmetric(transformation)) { |
| continue; |
| } |
| try { |
| assertInitEncryptSucceeds(transformation, good.build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForOriginationEnd(badEndDate) |
| .build()); |
| assertInitEncryptSucceeds(transformation, |
| TestUtils.buildUpon(good) |
| .setKeyValidityForConsumptionEnd(badEndDate) |
| .build()); |
| } catch (Throwable e) { |
| throw new RuntimeException("Failed for " + transformation, e); |
| } |
| } |
| } |
| |
| public void testEntropyConsumption() throws Exception { |
| // Assert that encryption consumes the correct amount of entropy from the provided |
| // SecureRandom and that decryption consumes no entropy. |
| |
| Provider provider = Security.getProvider(EXPECTED_PROVIDER_NAME); |
| assertNotNull(provider); |
| |
| CountingSecureRandom rng = new CountingSecureRandom(); |
| for (String transformation : EXPECTED_ALGORITHMS) { |
| for (ImportedKey key : importKatKeys( |
| transformation, |
| KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT, |
| true)) { |
| try { |
| Cipher cipher = Cipher.getInstance(transformation, provider); |
| Key encryptionKey = key.getKeystoreBackedEncryptionKey(); |
| byte[] plaintext = truncatePlaintextIfNecessary( |
| transformation, encryptionKey, new byte[32]); |
| if (plaintext == null) { |
| // Key too short to encrypt anything using this transformation. |
| continue; |
| } |
| Arrays.fill(plaintext, (byte) 0x1); |
| |
| // Cipher.init may only consume entropy for generating the IV. |
| rng.resetCounters(); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, rng); |
| int expectedEntropyBytesConsumedDuringInit; |
| String keyAlgorithm = TestUtils.getCipherKeyAlgorithm(transformation); |
| if (KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(keyAlgorithm)) { |
| String blockMode = |
| TestUtils.getCipherBlockMode(transformation).toUpperCase(Locale.US); |
| // Entropy should consumed for IV generation only. |
| switch (blockMode) { |
| case "ECB": |
| expectedEntropyBytesConsumedDuringInit = 0; |
| break; |
| case "CBC": |
| case "CTR": |
| expectedEntropyBytesConsumedDuringInit = 16; |
| break; |
| case "GCM": |
| expectedEntropyBytesConsumedDuringInit = 12; |
| break; |
| default: |
| throw new RuntimeException("Unsupported block mode " + blockMode); |
| } |
| } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { |
| expectedEntropyBytesConsumedDuringInit = 0; |
| } else { |
| throw new RuntimeException("Unsupported key algorithm: " + transformation); |
| } |
| assertEquals(expectedEntropyBytesConsumedDuringInit, rng.getOutputSizeBytes()); |
| AlgorithmParameters params = cipher.getParameters(); |
| |
| // Cipher.update should not consume entropy. |
| rng.resetCounters(); |
| byte[] ciphertext = cipher.update(plaintext); |
| assertEquals(0, rng.getOutputSizeBytes()); |
| |
| // Cipher.doFinal may consume entropy to pad the message (RSA only). |
| rng.resetCounters(); |
| ciphertext = TestUtils.concat(ciphertext, cipher.doFinal()); |
| int expectedEntropyBytesConsumedDuringDoFinal; |
| if (KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(keyAlgorithm)) { |
| expectedEntropyBytesConsumedDuringDoFinal = 0; |
| } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { |
| // Entropy should not be consumed during Cipher.init. |
| String encryptionPadding = |
| TestUtils.getCipherEncryptionPadding(transformation); |
| if (KeyProperties.ENCRYPTION_PADDING_RSA_OAEP.equalsIgnoreCase( |
| encryptionPadding)) { |
| int digestOutputSizeBits = |
| TestUtils.getDigestOutputSizeBits(TestUtils.getCipherDigest( |
| transformation)); |
| expectedEntropyBytesConsumedDuringDoFinal = |
| (digestOutputSizeBits + 7) / 8; |
| } else if (encryptionPadding.equalsIgnoreCase( |
| KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)) { |
| expectedEntropyBytesConsumedDuringDoFinal = |
| (TestUtils.getKeySizeBits(encryptionKey) + 7) / 8; |
| } else if (encryptionPadding.equalsIgnoreCase( |
| KeyProperties.ENCRYPTION_PADDING_NONE)) { |
| expectedEntropyBytesConsumedDuringDoFinal = 0; |
| } else { |
| throw new RuntimeException( |
| "Unexpected encryption padding: " + encryptionPadding); |
| } |
| } else { |
| throw new RuntimeException("Unsupported key algorithm: " + keyAlgorithm); |
| } |
| assertEquals( |
| expectedEntropyBytesConsumedDuringDoFinal, rng.getOutputSizeBytes()); |
| |
| // Assert that when initialization parameters are provided when encrypting, no |
| // entropy is consumed by Cipher.init. This is because Cipher.init should only |
| // use entropy for generating an IV which in this case no longer needs to be |
| // generated because it's specified in the parameters. |
| cipher = Cipher.getInstance(transformation, provider); |
| rng.resetCounters(); |
| cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, params, rng); |
| assertEquals(0, rng.getOutputSizeBytes()); |
| Key decryptionKey = key.getKeystoreBackedDecryptionKey(); |
| rng.resetCounters(); |
| cipher = Cipher.getInstance(transformation, provider); |
| cipher.init(Cipher.DECRYPT_MODE, decryptionKey, params, rng); |
| assertEquals(0, rng.getOutputSizeBytes()); |
| rng.resetCounters(); |
| cipher.update(ciphertext); |
| assertEquals(0, rng.getOutputSizeBytes()); |
| rng.resetCounters(); |
| cipher.doFinal(); |
| assertEquals(0, rng.getOutputSizeBytes()); |
| } catch (Throwable e) { |
| throw new RuntimeException( |
| "Failed for " + transformation + " with key " + key.getAlias(), e); |
| } |
| } |
| } |
| } |
| |
| private AlgorithmParameterSpec getWorkingDecryptionParameterSpec(String transformation) { |
| String keyAlgorithm = TestUtils.getCipherKeyAlgorithm(transformation); |
| if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { |
| return null; |
| } else if (KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(keyAlgorithm)) { |
| String blockMode = TestUtils.getCipherBlockMode(transformation); |
| if (KeyProperties.BLOCK_MODE_ECB.equalsIgnoreCase(blockMode)) { |
| return null; |
| } else if ((KeyProperties.BLOCK_MODE_CBC.equalsIgnoreCase(blockMode)) |
| || (KeyProperties.BLOCK_MODE_CTR.equalsIgnoreCase(blockMode))) { |
| return new IvParameterSpec( |
| new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}); |
| } else if (KeyProperties.BLOCK_MODE_GCM.equalsIgnoreCase(blockMode)) { |
| return new GCMParameterSpec( |
| 128, |
| new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}); |
| } else { |
| throw new IllegalArgumentException("Unsupported block mode: " + blockMode); |
| } |
| } else { |
| throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); |
| } |
| } |
| |
| private void assertInitDecryptSucceeds(String transformation, KeyProtection importParams) |
| throws Exception { |
| Cipher cipher = Cipher.getInstance(transformation, EXPECTED_PROVIDER_NAME); |
| Key key = |
| importDefaultKatKey(transformation, importParams).getKeystoreBackedDecryptionKey(); |
| AlgorithmParameterSpec params = getWorkingDecryptionParameterSpec(transformation); |
| cipher.init(Cipher.DECRYPT_MODE, key, params); |
| } |
| |
| private void assertInitDecryptThrowsInvalidKeyException( |
| String transformation, KeyProtection importParams) throws Exception { |
| Cipher cipher = Cipher.getInstance(transformation, EXPECTED_PROVIDER_NAME); |
| Key key = |
| importDefaultKatKey(transformation, importParams).getKeystoreBackedDecryptionKey(); |
| AlgorithmParameterSpec params = getWorkingDecryptionParameterSpec(transformation); |
| try { |
| cipher.init(Cipher.DECRYPT_MODE, key, params); |
| fail("InvalidKeyException should have been thrown"); |
| } catch (InvalidKeyException expected) {} |
| } |
| |
| private void assertInitEncryptSucceeds(String transformation, KeyProtection importParams) |
| throws Exception { |
| Cipher cipher = Cipher.getInstance(transformation, EXPECTED_PROVIDER_NAME); |
| Key key = |
| importDefaultKatKey(transformation, importParams).getKeystoreBackedEncryptionKey(); |
| cipher.init(Cipher.ENCRYPT_MODE, key); |
| } |
| |
| private void assertInitEncryptThrowsInvalidKeyException( |
| String transformation, KeyProtection importParams) throws Exception { |
| Cipher cipher = Cipher.getInstance(transformation, EXPECTED_PROVIDER_NAME); |
| Key key = |
| importDefaultKatKey(transformation, importParams).getKeystoreBackedEncryptionKey(); |
| try { |
| cipher.init(Cipher.ENCRYPT_MODE, key); |
| fail("InvalidKeyException should have been thrown"); |
| } catch (InvalidKeyException expected) {} |
| } |
| |
| private ImportedKey importDefaultKatKey( |
| String transformation, KeyProtection importParams) |
| throws Exception { |
| String keyAlgorithm = TestUtils.getCipherKeyAlgorithm(transformation); |
| if (KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(keyAlgorithm)) { |
| return TestUtils.importIntoAndroidKeyStore( |
| "testAES", |
| new SecretKeySpec(AES128_KAT_KEY_BYTES, "AES"), |
| importParams); |
| } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { |
| return TestUtils.importIntoAndroidKeyStore( |
| "testRSA", |
| getContext(), |
| R.raw.rsa_key2_pkcs8, |
| R.raw.rsa_key2_cert, |
| importParams); |
| } else { |
| throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); |
| } |
| } |
| |
| private ImportedKey importDefaultKatKey( |
| String transformation, int purposes, boolean ivProvidedWhenEncrypting) |
| throws Exception { |
| KeyProtection importParams = TestUtils.getMinimalWorkingImportParametersForCipheringWith( |
| transformation, purposes, ivProvidedWhenEncrypting); |
| return importDefaultKatKey(transformation, importParams); |
| } |
| |
| private Collection<ImportedKey> importKatKeys( |
| String transformation, int purposes, boolean ivProvidedWhenEncrypting) |
| throws Exception { |
| KeyProtection importParams = TestUtils.getMinimalWorkingImportParametersForCipheringWith( |
| transformation, purposes, ivProvidedWhenEncrypting); |
| String keyAlgorithm = TestUtils.getCipherKeyAlgorithm(transformation); |
| if (KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(keyAlgorithm)) { |
| return Arrays.asList( |
| TestUtils.importIntoAndroidKeyStore( |
| "testAES128", |
| new SecretKeySpec(AES128_KAT_KEY_BYTES, "AES"), |
| importParams), |
| TestUtils.importIntoAndroidKeyStore( |
| "testAES192", |
| new SecretKeySpec(AES192_KAT_KEY_BYTES, "AES"), |
| importParams), |
| TestUtils.importIntoAndroidKeyStore( |
| "testAES256", |
| new SecretKeySpec(AES256_KAT_KEY_BYTES, "AES"), |
| importParams) |
| ); |
| } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { |
| return RSASignatureTest.importKatKeyPairs(getContext(), importParams); |
| } else { |
| throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); |
| } |
| } |
| |
| private static boolean isSymmetric(String transformation) { |
| return TestUtils.isCipherSymmetric(transformation); |
| } |
| |
| private static byte[] truncatePlaintextIfNecessary( |
| String transformation, Key encryptionKey, byte[] plaintext) { |
| int maxSupportedPlaintextSizeBytes = |
| TestUtils.getMaxSupportedPlaintextInputSizeBytes( |
| transformation, encryptionKey); |
| if (plaintext.length <= maxSupportedPlaintextSizeBytes) { |
| // No need to truncate |
| return plaintext; |
| } else if (maxSupportedPlaintextSizeBytes < 0) { |
| // Key too short to encrypt anything at all using this transformation |
| return null; |
| } else { |
| // Truncate plaintext to exercise this transformation with this key |
| return Arrays.copyOf(plaintext, maxSupportedPlaintextSizeBytes); |
| } |
| } |
| } |