From b7b45073d2d5055bbf234cd9c73e94d7be008352 Mon Sep 17 00:00:00 2001 From: Will Tran Date: Fri, 1 Apr 2016 01:46:34 -0400 Subject: [PATCH] Bouncy Castle implementations of AES-256. Implments "AES/CBC/PKCS5Padding" and "AES/GCM/NoPadding" Fixes gh-2917 --- .../BouncyCastleAesBytesEncryptor.java | 77 +++++++++ .../BouncyCastleAesCbcBytesEncryptor.java | 83 ++++++++++ .../BouncyCastleAesGcmBytesEncryptor.java | 80 ++++++++++ ...astleAesBytesEncryptorEquivalencyTest.java | 150 ++++++++++++++++++ .../BouncyCastleAesBytesEncryptorTest.java | 79 +++++++++ 5 files changed, 469 insertions(+) create mode 100644 crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptor.java create mode 100644 crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java create mode 100644 crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTest.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTest.java diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptor.java new file mode 100644 index 00000000000..ea7e724a624 --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2011-2016 the original author or authors. + * + * 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 org.springframework.security.crypto.encrypt; + +import java.io.ByteArrayOutputStream; + +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.params.KeyParameter; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; + +/** + * Base class for AES-256 encryption using Bouncy Castle. + * + * @author William Tran + * + */ +abstract class BouncyCastleAesBytesEncryptor implements BytesEncryptor { + + final KeyParameter secretKey; + final BytesKeyGenerator ivGenerator; + + BouncyCastleAesBytesEncryptor(String password, CharSequence salt) { + this(password, salt, KeyGenerators.secureRandom(16)); + } + + BouncyCastleAesBytesEncryptor(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + if (ivGenerator.getKeyLength() != 16) { + throw new IllegalArgumentException("ivGenerator key length != block size 16"); + } + this.ivGenerator = ivGenerator; + PBEParametersGenerator keyGenerator = new PKCS5S2ParametersGenerator(); + byte[] pkcs12PasswordBytes = PBEParametersGenerator + .PKCS5PasswordToUTF8Bytes(password.toCharArray()); + keyGenerator.init(pkcs12PasswordBytes, Hex.decode(salt), 1024); + this.secretKey = (KeyParameter) keyGenerator.generateDerivedParameters(256); + } + + byte[] process(CipherOutputStream cipherOutputStream, + ByteArrayOutputStream byteArrayOutputStream, byte[] bytes) { + try { + cipherOutputStream.write(bytes); + // close() invokes the doFinal method of the encapsulated cipher object + // and flushes to the underlying outputStream. It must be called before + // we get the output. + cipherOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + catch (Throwable e) { + try { + // attempt release of resources + cipherOutputStream.close(); + } + catch (Throwable e1) { + } + throw new IllegalStateException("unable to encrypt/decrypt", e); + } + } + +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java new file mode 100644 index 00000000000..fa40bace896 --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2011-2016 the original author or authors. + * + * 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 org.springframework.security.crypto.encrypt; + +import static org.springframework.security.crypto.util.EncodingUtils.concatenate; +import static org.springframework.security.crypto.util.EncodingUtils.subArray; + +import java.io.ByteArrayOutputStream; + +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; + +/** + * An Encryptor equivalent to {@link AesBytesEncryptor} using + * {@link CipherAlgorithm#CBC} that uses Bouncy Castle instead of JCE. The + * algorithm is equivalent to "AES/CBC/PKCS5Padding". + * + * @author William Tran + * + */ +public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryptor { + + public BouncyCastleAesCbcBytesEncryptor(String password, CharSequence salt) { + super(password, salt); + } + + public BouncyCastleAesCbcBytesEncryptor(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + super(password, salt, ivGenerator); + } + + @Override + public byte[] encrypt(byte[] bytes) { + byte[] iv = this.ivGenerator.generateKey(); + + PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESFastEngine()), new PKCS7Padding()); + blockCipher.init(true, new ParametersWithIV(secretKey, iv)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( + blockCipher.getOutputSize(bytes.length)); + CipherOutputStream cipherOutputStream = new CipherOutputStream( + byteArrayOutputStream, blockCipher); + + byte[] encrypted = process(cipherOutputStream, byteArrayOutputStream, bytes); + return iv != null ? concatenate(iv, encrypted) : encrypted; + } + + @Override + public byte[] decrypt(byte[] encryptedBytes) { + byte[] iv = subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength()); + encryptedBytes = subArray(encryptedBytes, this.ivGenerator.getKeyLength(), + encryptedBytes.length); + + PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESFastEngine()), new PKCS7Padding()); + blockCipher.init(false, new ParametersWithIV(secretKey, iv)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( + blockCipher.getOutputSize(encryptedBytes.length)); + CipherOutputStream cipherOutputStream = new CipherOutputStream( + byteArrayOutputStream, blockCipher); + + return process(cipherOutputStream, byteArrayOutputStream, encryptedBytes); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java new file mode 100644 index 00000000000..62f92fab13c --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011-2016 the original author or authors. + * + * 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 org.springframework.security.crypto.encrypt; + +import static org.springframework.security.crypto.util.EncodingUtils.concatenate; +import static org.springframework.security.crypto.util.EncodingUtils.subArray; + +import java.io.ByteArrayOutputStream; + +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; + +/** + * An Encryptor equivalent to {@link AesBytesEncryptor} using + * {@link CipherAlgorithm#GCM} that uses Bouncy Castle instead of JCE. The + * algorithm is equivalent to "AES/GCM/NoPadding". + * + * @author William Tran + * + */ +public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryptor { + + public BouncyCastleAesGcmBytesEncryptor(String password, CharSequence salt) { + super(password, salt); + } + + public BouncyCastleAesGcmBytesEncryptor(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + super(password, salt, ivGenerator); + } + + @Override + public byte[] encrypt(byte[] bytes) { + byte[] iv = this.ivGenerator.generateKey(); + + GCMBlockCipher blockCipher = new GCMBlockCipher(new AESFastEngine()); + blockCipher.init(true, new AEADParameters(secretKey, 128, iv)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( + blockCipher.getOutputSize(bytes.length)); + CipherOutputStream cipherOutputStream = new CipherOutputStream(byteArrayOutputStream, + blockCipher); + + byte[] encrypted = process(cipherOutputStream, byteArrayOutputStream, bytes); + return iv != null ? concatenate(iv, encrypted) : encrypted; + } + + @Override + public byte[] decrypt(byte[] encryptedBytes) { + byte[] iv = subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength()); + encryptedBytes = subArray(encryptedBytes, this.ivGenerator.getKeyLength(), + encryptedBytes.length); + + GCMBlockCipher blockCipher = new GCMBlockCipher(new AESFastEngine()); + blockCipher.init(false, new AEADParameters(secretKey, 128, iv)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( + blockCipher.getOutputSize(encryptedBytes.length)); + CipherOutputStream cipherOutputStream = new CipherOutputStream( + byteArrayOutputStream, blockCipher); + + return process(cipherOutputStream, byteArrayOutputStream, encryptedBytes); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTest.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTest.java new file mode 100644 index 00000000000..94c2649b8b9 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2011-2016 the original author or authors. + * + * 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 org.springframework.security.crypto.encrypt; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; + +public class BouncyCastleAesBytesEncryptorEquivalencyTest { + + private byte[] testData; + private String password; + private String salt; + + @Before + public void setup() { + Assume.assumeTrue( + "couldn't create AesBytesEncryptor, is JCE unlimited strength enabled?", + isAes256Available()); + + // generate random password, salt, and test data + SecureRandom secureRandom = new SecureRandom(); + password = UUID.randomUUID().toString(); + byte[] saltBytes = new byte[16]; + secureRandom.nextBytes(saltBytes); + salt = new String(Hex.encode(saltBytes)); + testData = new byte[1024 * 1024]; + secureRandom.nextBytes(testData); + } + + @Test + public void bouncyCastleAesCbcWithPredictableIvEquvalent() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesCbcBytesEncryptor(password, salt, + new PredictableRandomBytesKeyGenerator(16)); + BytesEncryptor jceEncryptor = new AesBytesEncryptor(password, salt, + new PredictableRandomBytesKeyGenerator(16)); + testEquivalence(bcEncryptor, jceEncryptor); + } + + @Test + public void bouncyCastleAesCbcWithSecureIvCompatible() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesCbcBytesEncryptor(password, salt, + KeyGenerators.secureRandom(16)); + BytesEncryptor jceEncryptor = new AesBytesEncryptor(password, salt, + KeyGenerators.secureRandom(16)); + testCompatibility(bcEncryptor, jceEncryptor); + } + + @Test + public void bouncyCastleAesGcmWithPredictableIvEquvalent() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesGcmBytesEncryptor(password, salt, + new PredictableRandomBytesKeyGenerator(16)); + BytesEncryptor jceEncryptor = new AesBytesEncryptor(password, salt, + new PredictableRandomBytesKeyGenerator(16), CipherAlgorithm.GCM); + testEquivalence(bcEncryptor, jceEncryptor); + } + + @Test + public void bouncyCastleAesGcmWithSecureIvCompatible() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesGcmBytesEncryptor(password, salt, + KeyGenerators.secureRandom(16)); + BytesEncryptor jceEncryptor = new AesBytesEncryptor(password, salt, + KeyGenerators.secureRandom(16), CipherAlgorithm.GCM); + testCompatibility(bcEncryptor, jceEncryptor); + } + + private void testEquivalence(BytesEncryptor left, BytesEncryptor right) + throws Exception { + // tests that right and left generate the same encrypted bytes + // and can decrypt back to the original input + byte[] leftEncrypted = left.encrypt(testData); + byte[] rightEncrypted = right.encrypt(testData); + Assert.assertArrayEquals(leftEncrypted, rightEncrypted); + byte[] leftDecrypted = left.decrypt(leftEncrypted); + byte[] rightDecrypted = right.decrypt(rightEncrypted); + Assert.assertArrayEquals(testData, leftDecrypted); + Assert.assertArrayEquals(testData, rightDecrypted); + } + + private void testCompatibility(BytesEncryptor left, BytesEncryptor right) + throws Exception { + // tests that right can decrypt what left encrypted and vice versa + // and that the decypted data is the same as the original + byte[] leftEncrypted = left.encrypt(testData); + byte[] rightEncrypted = right.encrypt(testData); + byte[] leftDecrypted = left.decrypt(rightEncrypted); + byte[] rightDecrypted = right.decrypt(leftEncrypted); + Assert.assertArrayEquals(testData, leftDecrypted); + Assert.assertArrayEquals(testData, rightDecrypted); + } + + private boolean isAes256Available() { + try { + return javax.crypto.Cipher.getMaxAllowedKeyLength("AES") >= 256; + } + catch (Exception e) { + return false; + } + + } + + /** + * A BytesKeyGenerator that always generates the same sequence of values + */ + private static class PredictableRandomBytesKeyGenerator implements BytesKeyGenerator { + + private final Random random; + + private final int keyLength; + + public PredictableRandomBytesKeyGenerator(int keyLength) { + this.random = new Random(1); + this.keyLength = keyLength; + } + + public int getKeyLength() { + return keyLength; + } + + public byte[] generateKey() { + byte[] bytes = new byte[keyLength]; + random.nextBytes(bytes); + return bytes; + } + + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTest.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTest.java new file mode 100644 index 00000000000..4521bed9e23 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2011-2016 the original author or authors. + * + * 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 org.springframework.security.crypto.encrypt; + +import java.security.SecureRandom; +import java.util.UUID; + +import org.bouncycastle.util.Arrays; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.keygen.KeyGenerators; + +public class BouncyCastleAesBytesEncryptorTest { + + private byte[] testData; + private String password; + private String salt; + + @Before + public void setup() { + // generate random password, salt, and test data + SecureRandom secureRandom = new SecureRandom(); + password = UUID.randomUUID().toString(); + byte[] saltBytes = new byte[16]; + secureRandom.nextBytes(saltBytes); + salt = new String(Hex.encode(saltBytes)); + testData = new byte[1024 * 1024]; + secureRandom.nextBytes(testData); + } + + @Test + public void bcCbcWithSecureIvGeneratesDifferentMessages() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesCbcBytesEncryptor(password, salt); + generatesDifferentCipherTexts(bcEncryptor); + } + + @Test + public void bcGcmWithSecureIvGeneratesDifferentMessages() throws Exception { + BytesEncryptor bcEncryptor = new BouncyCastleAesGcmBytesEncryptor(password, salt); + generatesDifferentCipherTexts(bcEncryptor); + } + + private void generatesDifferentCipherTexts(BytesEncryptor bcEncryptor) { + byte[] encrypted1 = bcEncryptor.encrypt(testData); + byte[] encrypted2 = bcEncryptor.encrypt(testData); + Assert.assertFalse(Arrays.areEqual(encrypted1, encrypted2)); + byte[] decrypted1 = bcEncryptor.decrypt(encrypted1); + byte[] decrypted2 = bcEncryptor.decrypt(encrypted2); + Assert.assertArrayEquals(testData, decrypted1); + Assert.assertArrayEquals(testData, decrypted2); + } + + @Test(expected = IllegalArgumentException.class) + public void bcCbcWithWrongLengthIv() throws Exception { + new BouncyCastleAesCbcBytesEncryptor(password, salt, + KeyGenerators.secureRandom(8)); + } + + @Test(expected = IllegalArgumentException.class) + public void bcGcmWithWrongLengthIv() throws Exception { + new BouncyCastleAesGcmBytesEncryptor(password, salt, + KeyGenerators.secureRandom(8)); + } +}