1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package libcore.javax.crypto; 18 19import junit.framework.TestCase; 20 21import java.io.ByteArrayInputStream; 22import java.io.ByteArrayOutputStream; 23import java.io.FilterInputStream; 24import java.io.IOException; 25import java.io.InputStream; 26import java.lang.reflect.Method; 27import java.security.NoSuchAlgorithmException; 28import java.security.Provider; 29import java.security.Security; 30import java.security.spec.AlgorithmParameterSpec; 31import java.util.Arrays; 32import javax.crypto.AEADBadTagException; 33import javax.crypto.Cipher; 34import javax.crypto.CipherInputStream; 35import javax.crypto.KeyGenerator; 36import javax.crypto.SecretKey; 37import javax.crypto.ShortBufferException; 38import javax.crypto.spec.GCMParameterSpec; 39import javax.crypto.spec.IvParameterSpec; 40import javax.crypto.spec.SecretKeySpec; 41 42import static org.mockito.Mockito.mock; 43import static org.mockito.Mockito.times; 44import static org.mockito.Mockito.verify; 45 46public final class CipherInputStreamTest extends TestCase { 47 48 private final byte[] aesKeyBytes = { 49 (byte) 0x50, (byte) 0x98, (byte) 0xF2, (byte) 0xC3, (byte) 0x85, (byte) 0x23, 50 (byte) 0xA3, (byte) 0x33, (byte) 0x50, (byte) 0x98, (byte) 0xF2, (byte) 0xC3, 51 (byte) 0x85, (byte) 0x23, (byte) 0xA3, (byte) 0x33, 52 }; 53 54 private final byte[] aesIvBytes = { 55 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, 56 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, 57 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, 58 }; 59 60 private final byte[] aesCipherText = { 61 (byte) 0x2F, (byte) 0x2C, (byte) 0x74, (byte) 0x31, (byte) 0xFF, (byte) 0xCC, 62 (byte) 0x28, (byte) 0x7D, (byte) 0x59, (byte) 0xBD, (byte) 0xE5, (byte) 0x0A, 63 (byte) 0x30, (byte) 0x7E, (byte) 0x6A, (byte) 0x4A 64 }; 65 66 private final byte[] rc4CipherText = { 67 (byte) 0x88, (byte) 0x01, (byte) 0xE3, (byte) 0x52, (byte) 0x7B 68 }; 69 70 private final String plainText = "abcde"; 71 private SecretKey key; 72 private SecretKey rc4Key; 73 private AlgorithmParameterSpec iv; 74 75 @Override protected void setUp() throws Exception { 76 key = new SecretKeySpec(aesKeyBytes, "AES"); 77 rc4Key = new SecretKeySpec(aesKeyBytes, "RC4"); 78 iv = new IvParameterSpec(aesIvBytes); 79 } 80 81 private static class MeasuringInputStream extends FilterInputStream { 82 private int totalRead; 83 84 protected MeasuringInputStream(InputStream in) { 85 super(in); 86 } 87 88 @Override 89 public int read() throws IOException { 90 int c = super.read(); 91 totalRead++; 92 return c; 93 } 94 95 @Override 96 public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { 97 int numRead = super.read(buffer, byteOffset, byteCount); 98 if (numRead != -1) { 99 totalRead += numRead; 100 } 101 return numRead; 102 } 103 104 public int getTotalRead() { 105 return totalRead; 106 } 107 } 108 109 public void testAvailable() throws Exception { 110 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 111 cipher.init(Cipher.DECRYPT_MODE, key, iv); 112 MeasuringInputStream in = new MeasuringInputStream(new ByteArrayInputStream(aesCipherText)); 113 InputStream cin = new CipherInputStream(in, cipher); 114 assertTrue(cin.read() != -1); 115 assertEquals(aesCipherText.length, in.getTotalRead()); 116 } 117 118 public void testDecrypt_NullInput_Discarded() throws Exception { 119 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 120 cipher.init(Cipher.DECRYPT_MODE, key, iv); 121 InputStream in = new CipherInputStream(new ByteArrayInputStream(aesCipherText), cipher); 122 int discard = 3; 123 while (discard != 0) { 124 discard -= in.read(null, 0, discard); 125 } 126 byte[] bytes = readAll(in); 127 assertEquals(Arrays.toString(plainText.substring(3).getBytes("UTF-8")), 128 Arrays.toString(bytes)); 129 } 130 131 public void testEncrypt() throws Exception { 132 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 133 cipher.init(Cipher.ENCRYPT_MODE, key, iv); 134 InputStream in = new CipherInputStream( 135 new ByteArrayInputStream(plainText.getBytes("UTF-8")), cipher); 136 byte[] bytes = readAll(in); 137 assertEquals(Arrays.toString(aesCipherText), Arrays.toString(bytes)); 138 139 // Reading again shouldn't throw an exception. 140 assertEquals(-1, in.read()); 141 } 142 143 public void testEncrypt_RC4() throws Exception { 144 Cipher cipher = Cipher.getInstance("RC4"); 145 cipher.init(Cipher.ENCRYPT_MODE, rc4Key); 146 InputStream in = new CipherInputStream( 147 new ByteArrayInputStream(plainText.getBytes("UTF-8")), cipher); 148 byte[] bytes = readAll(in); 149 assertEquals(Arrays.toString(rc4CipherText), Arrays.toString(bytes)); 150 151 // Reading again shouldn't throw an exception. 152 assertEquals(-1, in.read()); 153 } 154 155 public void testDecrypt() throws Exception { 156 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 157 cipher.init(Cipher.DECRYPT_MODE, key, iv); 158 InputStream in = new CipherInputStream(new ByteArrayInputStream(aesCipherText), cipher); 159 byte[] bytes = readAll(in); 160 assertEquals(Arrays.toString(plainText.getBytes("UTF-8")), Arrays.toString(bytes)); 161 } 162 163 public void testDecrypt_RC4() throws Exception { 164 Cipher cipher = Cipher.getInstance("RC4"); 165 cipher.init(Cipher.DECRYPT_MODE, rc4Key); 166 InputStream in = new CipherInputStream(new ByteArrayInputStream(rc4CipherText), cipher); 167 byte[] bytes = readAll(in); 168 assertEquals(Arrays.toString(plainText.getBytes("UTF-8")), Arrays.toString(bytes)); 169 } 170 171 public void testSkip() throws Exception { 172 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 173 cipher.init(Cipher.DECRYPT_MODE, key, iv); 174 InputStream in = new CipherInputStream(new ByteArrayInputStream(aesCipherText), cipher); 175 assertTrue(in.skip(5) >= 0); 176 } 177 178 private byte[] readAll(InputStream in) throws IOException { 179 ByteArrayOutputStream out = new ByteArrayOutputStream(); 180 int count; 181 byte[] buffer = new byte[1024]; 182 while ((count = in.read(buffer)) != -1) { 183 out.write(buffer, 0, count); 184 } 185 return out.toByteArray(); 186 } 187 188 public void testCipherInputStream_TruncatedInput_Failure() throws Exception { 189 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 190 cipher.init(Cipher.DECRYPT_MODE, key, iv); 191 InputStream is = new CipherInputStream(new ByteArrayInputStream(new byte[31]), cipher); 192 is.read(new byte[4]); 193 is.close(); 194 } 195 196 public void testCipherInputStream_NullInputStream_Failure() throws Exception { 197 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 198 cipher.init(Cipher.DECRYPT_MODE, key, iv); 199 InputStream is = new CipherInputStream(null, cipher); 200 try { 201 is.read(); 202 fail("Expected NullPointerException"); 203 } catch (NullPointerException expected) { 204 } 205 206 byte[] buffer = new byte[128]; 207 try { 208 is.read(buffer); 209 fail("Expected NullPointerException"); 210 } catch (NullPointerException expected) { 211 } 212 213 try { 214 is.read(buffer, 0, buffer.length); 215 fail("Expected NullPointerException"); 216 } catch (NullPointerException expected) { 217 } 218 } 219 220 public void testCloseTwice() throws Exception { 221 InputStream mockIs = mock(InputStream.class); 222 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 223 cipher.init(Cipher.DECRYPT_MODE, key, iv); 224 225 CipherInputStream cis = new CipherInputStream(mockIs, cipher); 226 cis.close(); 227 cis.close(); 228 229 verify(mockIs, times(1)).close(); 230 } 231 232 /** 233 * CipherSpi that increments it's engineGetOutputSize output when 234 * engineUpdate is called. 235 */ 236 public static class CipherSpiWithGrowingOutputSize extends MockCipherSpi { 237 private int outputSizeDelta = 0; 238 239 @Override 240 protected int engineGetOutputSize(int inputLen) { 241 return inputLen + outputSizeDelta; 242 } 243 244 @Override 245 protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, 246 int outputOffset) throws ShortBufferException { 247 int expectedOutputSize = inputLen + outputSizeDelta++; 248 if ((output.length - outputOffset) < expectedOutputSize) { 249 throw new ShortBufferException(); 250 } 251 return expectedOutputSize; 252 } 253 254 @Override 255 protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { 256 int expectedOutputSize = inputLen + outputSizeDelta++; 257 return new byte[expectedOutputSize]; 258 } 259 260 @Override 261 protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) { 262 return input; 263 } 264 } 265 266 private static class MockProvider extends Provider { 267 public MockProvider() { 268 super("MockProvider", 1.0, "Mock provider used for testing"); 269 put("Cipher.GrowingOutputSize", 270 CipherSpiWithGrowingOutputSize.class.getName()); 271 } 272 } 273 274 // http://b/32643789, check that CipherSpi.engineGetOutputSize is called and applied 275 // to output buffer size before calling CipherSpi.egineUpdate(byte[],int,int,byte[],int). 276 public void testCipherOutputSizeChange() throws Exception { 277 Provider mockProvider = new MockProvider(); 278 279 Cipher cipher = Cipher.getInstance("GrowingOutputSize", mockProvider); 280 281 cipher.init(Cipher.DECRYPT_MODE, key, iv); 282 InputStream mockEncryptedInputStream = new ByteArrayInputStream(new byte[1024]); 283 try (InputStream is = new CipherInputStream(mockEncryptedInputStream, cipher)) { 284 byte[] buffer = new byte[1024]; 285 // engineGetOutputSize returns 512+0, engineUpdate expects buf >= 512 286 assertEquals(512, is.read(buffer)); 287 // engineGetOutputSize returns 512+1, engineUpdate expects buf >= 513 288 // and will throw ShortBufferException buffer is smaller. 289 assertEquals(513, is.read(buffer)); 290 } 291 } 292 293 // From b/31590622. CipherInputStream had a bug where it would ignore exceptions 294 // thrown during close(), because it was expecting exceptions to be thrown by read(). 295 public void testDecryptCorruptGCM() throws Exception { 296 for (Provider provider : Security.getProviders()) { 297 Cipher cipher; 298 try { 299 cipher = Cipher.getInstance("AES/GCM/NoPadding", provider); 300 } catch (NoSuchAlgorithmException e) { 301 continue; 302 } 303 SecretKey key; 304 if (provider.getName().equals("AndroidKeyStoreBCWorkaround")) { 305 key = getAndroidKeyStoreSecretKey(); 306 } else { 307 KeyGenerator keygen = KeyGenerator.getInstance("AES"); 308 keygen.init(256); 309 key = keygen.generateKey(); 310 } 311 GCMParameterSpec params = new GCMParameterSpec(128, new byte[12]); 312 byte[] unencrypted = new byte[200]; 313 314 // Normal providers require specifying the IV, but KeyStore prohibits it, so 315 // we have to special-case it 316 if (provider.getName().equals("AndroidKeyStoreBCWorkaround")) { 317 cipher.init(Cipher.ENCRYPT_MODE, key); 318 } else { 319 cipher.init(Cipher.ENCRYPT_MODE, key, params); 320 } 321 byte[] encrypted = cipher.doFinal(unencrypted); 322 323 // Corrupt the final byte, which will corrupt the authentication tag 324 encrypted[encrypted.length - 1] ^= 1; 325 326 cipher.init(Cipher.DECRYPT_MODE, key, params); 327 CipherInputStream cis = new CipherInputStream( 328 new ByteArrayInputStream(encrypted), cipher); 329 try { 330 cis.read(unencrypted); 331 cis.close(); 332 fail("Reading a corrupted stream should throw an exception." 333 + " Provider: " + provider); 334 } catch (IOException expected) { 335 assertTrue(expected.getCause() instanceof AEADBadTagException); 336 } 337 } 338 339 } 340 341 // The AndroidKeyStoreBCWorkaround provider can't use keys created by anything 342 // but Android KeyStore, which requires using its own parameters class to create 343 // keys. Since we're in javax, we can't link against the frameworks classes, so 344 // we have to use reflection to make a suitable key. This will always be safe 345 // because if we're making a key for AndroidKeyStoreBCWorkaround, the KeyStore 346 // classes must be present. 347 private static SecretKey getAndroidKeyStoreSecretKey() throws Exception { 348 KeyGenerator keygen = KeyGenerator.getInstance("AES", "AndroidKeyStore"); 349 Class<?> keyParamsBuilderClass = keygen.getClass().getClassLoader().loadClass( 350 "android.security.keystore.KeyGenParameterSpec$Builder"); 351 Object keyParamsBuilder = keyParamsBuilderClass.getConstructor(String.class, Integer.TYPE) 352 // 3 is PURPOSE_ENCRYPT | PURPOSE_DECRYPT 353 .newInstance("testDecryptCorruptGCM", 3); 354 keyParamsBuilderClass.getMethod("setBlockModes", new Class[]{String[].class}) 355 .invoke(keyParamsBuilder, new Object[]{new String[]{"GCM"}}); 356 keyParamsBuilderClass.getMethod("setEncryptionPaddings", new Class[]{String[].class}) 357 .invoke(keyParamsBuilder, new Object[]{new String[]{"NoPadding"}}); 358 AlgorithmParameterSpec spec = (AlgorithmParameterSpec) 359 keyParamsBuilderClass.getMethod("build", new Class[]{}).invoke(keyParamsBuilder); 360 keygen.init(spec); 361 return keygen.generateKey(); 362 } 363} 364