Răsfoiți Sursa

Bouncy Castle implementations of AES-256

Adds "AES/CBC/PKCS5Padding" and "AES/GCM/NoPadding"

Fixes gh-2917
Will Tran 9 ani în urmă
părinte
comite
63b2cfe1cf

+ 77 - 0
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);
+		}
+	}
+
+}

+ 83 - 0
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);
+	}
+}

+ 80 - 0
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);
+	}
+
+}

+ 150 - 0
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;
+		}
+
+	}
+
+}

+ 79 - 0
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));
+	}
+}