/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.crypto; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Map; import java.util.Random; import org.apache.commons.io.IOUtils; import org.testng.Assert; import org.testng.annotations.Test; import com.google.common.collect.ImmutableMap; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; public class RotatingAESCodecTest { @Test public void testStreams() throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { final byte[] toWrite = "hello world".getBytes(); SimpleCredentialStore credStore = new SimpleCredentialStore(); RotatingAESCodec encryptor = new RotatingAESCodec(credStore); ByteArrayOutputStream sink = new ByteArrayOutputStream(); OutputStream os = encryptor.encodeOutputStream(sink); os.write(toWrite); os.close(); byte[] encryptedBytes = sink.toByteArray(); manuallyDecodeAndVerifyBytes(toWrite, encryptedBytes, credStore); // Try with stream InputStream decoderIn = encryptor.decodeInputStream(new ByteArrayInputStream(encryptedBytes)); byte[] decoded = IOUtils.toByteArray(decoderIn); Assert.assertEquals(decoded, toWrite, "Expected decoded output to match encoded output"); } @Test public void testLotsOfData() throws Exception { long bytesToWrite = 20 * 1000 * 1000; byte[] buf = new byte[16384]; long bytesWritten = 0; SimpleCredentialStore credStore = new SimpleCredentialStore(); RotatingAESCodec encryptor = new RotatingAESCodec(credStore); ByteArrayOutputStream sink = new ByteArrayOutputStream(); ByteArrayOutputStream originalBytesStream = new ByteArrayOutputStream(); OutputStream encryptedStream = encryptor.encodeOutputStream(sink); Random r = new Random(); while (bytesWritten < bytesToWrite) { r.nextBytes(buf); originalBytesStream.write(buf); encryptedStream.write(buf); bytesWritten += buf.length; } encryptedStream.close(); byte[] originalBytes = originalBytesStream.toByteArray(); byte[] encryptedBytes = sink.toByteArray(); manuallyDecodeAndVerifyBytes(originalBytes, encryptedBytes, credStore); // Try with stream InputStream decoderIn = encryptor.decodeInputStream(new ByteArrayInputStream(encryptedBytes)); byte[] decoded = IOUtils.toByteArray(decoderIn); Assert.assertEquals(decoded, originalBytes, "Expected decoded output to match encoded output"); } private byte[] readAndBase64DecodeBody(InputStream in) throws IOException { byte[] body = IOUtils.toByteArray(in); body = DatatypeConverter.parseBase64Binary(new String(body, "UTF-8")); return body; } private byte[] verifyAndExtractIv(InputStream in, Integer ivLen) throws IOException { int bytesRead; byte[] base64Iv = new byte[ivLen]; bytesRead = in.read(base64Iv); Assert.assertEquals(Integer.valueOf(bytesRead), Integer.valueOf(ivLen), "Expected to read IV"); return DatatypeConverter.parseBase64Binary(new String(base64Iv, "UTF-8")); } private Integer verifyIvLen(InputStream in) throws IOException { int bytesRead; byte[] ivLenBytes = new byte[3]; bytesRead = in.read(ivLenBytes); Assert.assertEquals(bytesRead, ivLenBytes.length, "Expected to be able to iv length"); Integer ivLen = Integer.valueOf(new String(ivLenBytes, "UTF-8")); Assert.assertEquals(Integer.valueOf(ivLen), Integer.valueOf(24), "Always expect IV to be 24 bytes base64 encoded"); return ivLen; } private void verifyKeyId(InputStream in, int expectedKeyId) throws IOException { // Verify keyId is properly padded byte[] keyIdBytes = new byte[4]; int bytesRead = in.read(keyIdBytes); Assert.assertEquals(bytesRead, keyIdBytes.length, "Expected to be able to read key id"); String keyId = new String(keyIdBytes, "UTF-8"); Assert.assertEquals(Integer.valueOf(keyId), Integer.valueOf(expectedKeyId), "Expected keyId to equal 1"); } private void manuallyDecodeAndVerifyBytes(byte[] originalBytes, byte[] encryptedBytes, SimpleCredentialStore credStore) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { // Manually decode InputStream in = new ByteArrayInputStream(encryptedBytes); verifyKeyId(in, 1); Integer ivLen = verifyIvLen(in); byte[] ivBinary = verifyAndExtractIv(in, ivLen); byte[] body = readAndBase64DecodeBody(in); // feed back into cipheroutput stream Cipher inputCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBinary); inputCipher.init(Cipher.DECRYPT_MODE, credStore.getKey(), ivParameterSpec); CipherInputStream cis = new CipherInputStream(new ByteArrayInputStream(body), inputCipher); byte[] decoded = IOUtils.toByteArray(cis); Assert.assertEquals(decoded, originalBytes, "Expected decoded output to match encoded output"); } static class SimpleCredentialStore implements CredentialStore { private final SecretKey key; public SimpleCredentialStore() { SecureRandom r = new SecureRandom(); byte[] keyBytes = new byte[16]; r.nextBytes(keyBytes); key = new SecretKeySpec(keyBytes, "AES"); } @Override public byte[] getEncodedKey(String id) { if (id.equals("1")) { return key.getEncoded(); } return null; } @Override public Map<String, byte[]> getAllEncodedKeys() { return ImmutableMap.of("1", key.getEncoded()); } public SecretKey getKey() { return key; } } }