Skip to content

Instantly share code, notes, and snippets.

@garcia-jj
Last active June 19, 2016 22:33
Show Gist options
  • Save garcia-jj/ee178bcf5a369c8cca48 to your computer and use it in GitHub Desktop.
Save garcia-jj/ee178bcf5a369c8cca48 to your computer and use it in GitHub Desktop.
package siscom.model.security;
import static java.util.Objects.requireNonNull;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.security.auth.DestroyFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class to create a password based encryptor using a strong cryptography algorithm. This class have
* dependency of Bouncy Castle API and requires Unlimited Strength Jurisdiction files inside Java runtime.
*
* @author Otávio Garcia
*/
public final class PasswordBasedEncryptor {
private static final Logger LOGGER = LoggerFactory.getLogger(PasswordBasedEncryptor.class);
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final String PBE_ALGORITHM = "PBEWITHSHA256AND256BITAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CTR/PKCS7Padding";
private static final int ITERATIONS = 2000, SALT_LENGTH = 100;
private final char[] passphrase;
/**
* Creates an instance for {@link PasswordBasedEncryptor} using first arg as passphrase. The passphrase
* will cloned after instantiation to avoid later changes.
*
* @param passphrase The passphrase used to encryption and decryption operations, and can't be null or
* empty.
*/
public PasswordBasedEncryptor(final char[] passphrase) {
requireNonNull(passphrase, "Passphrase can't be null");
requireNotEmpty(passphrase, "Passphrase can't be empty");
this.passphrase = Arrays.copyOf(passphrase, passphrase.length);
}
public byte[] doEncryption(final byte[] input) {
LOGGER.trace(">doEncryption");
if (input == null || input.length == 0) {
return emptyByteArray();
}
final byte[] salt = generateRandomSalt();
final SecretKey key = createPBEKey(passphrase, salt);
final Cipher cipher = createCipherInstance(Cipher.ENCRYPT_MODE, key);
final byte[] ciphertext = doFinal(input, cipher);
final byte[] output = Arrays.copyOf(salt, salt.length + ciphertext.length);
System.arraycopy(ciphertext, 0, output, salt.length, ciphertext.length);
destroyQuietly(key);
fillWithFixedBytes(salt);
fillWithFixedBytes(ciphertext);
return output;
}
public byte[] doDecryption(final byte[] input) {
LOGGER.trace(">doDecryption");
if (input == null || input.length == 0) {
return emptyByteArray();
}
final byte[] retrievedSalt = Arrays.copyOfRange(input, 0, SALT_LENGTH);
final byte[] unsaltedInput = Arrays.copyOfRange(input, SALT_LENGTH, input.length);
final SecretKey key = createPBEKey(passphrase, retrievedSalt);
final Cipher cipher = createCipherInstance(Cipher.DECRYPT_MODE, key);
final byte[] out = doFinal(unsaltedInput, cipher);
destroyQuietly(key);
fillWithFixedBytes(retrievedSalt);
fillWithFixedBytes(unsaltedInput);
return out;
}
private SecretKey createPBEKey(final char[] passphrase, final byte[] salt) {
try {
LOGGER.trace("creating new PBE key");
final SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM);
final KeySpec keySpec = new PBEKeySpec(passphrase, salt, ITERATIONS);
return factory.generateSecret(keySpec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
LOGGER.warn("An error occur when create PBE key", e);
throw new UnsupportedOperationException(e);
}
}
private Cipher createCipherInstance(final int mode, final SecretKey key) {
try {
LOGGER.trace("creating new cipher instance");
final Cipher instance = Cipher.getInstance(CIPHER_ALGORITHM);
instance.init(mode, key, instance.getParameters(), secureRandom());
return instance;
} catch (final GeneralSecurityException e) {
LOGGER.warn("An error occur when create chipher instance", e);
throw new UnsupportedOperationException(e);
}
}
private byte[] doFinal(final byte[] input, final Cipher cipher) {
try {
return cipher.doFinal(input);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
public byte[] generateRandomSalt() {
final byte[] bytes = new byte[SALT_LENGTH];
secureRandom().nextBytes(bytes);
return bytes;
}
private SecureRandom secureRandom() {
try {
return SecureRandom.getInstance(RANDOM_ALGORITHM);
} catch (final NoSuchAlgorithmException e) {
LOGGER.warn("An error occur when create a SecureRandom instance", e);
throw new UnsupportedOperationException(e);
}
}
private void destroyQuietly(final SecretKey key) {
try {
LOGGER.trace("trying to destroy secret key");
key.destroy();
} catch (final DestroyFailedException e) {
LOGGER.trace("An error occur when destroy secret key", e);
}
}
private void requireNotEmpty(final char[] arr, final String message) {
if (arr.length == 0) {
throw new IllegalArgumentException(message);
}
}
private byte[] emptyByteArray() {
return new byte[0];
}
private void fillWithFixedBytes(final byte[] salt) {
LOGGER.trace("filling data with some bytes");
Arrays.fill(salt, (byte) 0xFF);
}
}
package siscom.model.security;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.UUID.randomUUID;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import java.util.Base64;
import java.util.UUID;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class PasswordBasedEncryptorTest {
@Rule
public ExpectedException exceptions = ExpectedException.none();
private char[] passphrase;
@Before
public void setup() {
passphrase = UUID.randomUUID().toString().toCharArray();
}
@Test
public void shouldThrowsExceptionWhenPassphraseIsNull() {
exceptions.expect(NullPointerException.class);
exceptions.expectMessage("Passphrase can't be null");
new PasswordBasedEncryptor((char[]) null);
}
@Test
public void shouldThrowsExceptionWhenPassphraseIsEmpty() {
exceptions.expect(IllegalArgumentException.class);
exceptions.expectMessage("Passphrase can't be empty");
new PasswordBasedEncryptor(new char[0]);
}
@Test
public void shouldReturnEmptyArrayValueWhenEncryptNullInput() {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doEncryption(null), is(new byte[0]));
}
@Test
public void shouldReturnEmptyArrayValueWhenDecryptNullInput() {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doDecryption(null), is(new byte[0]));
}
@Test
public void testEncryptionWhenInputValueIsNull() throws Exception {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doEncryption(null), is(new byte[0]));
}
@Test
public void testEncryptionWhenInputValueIsEmpty() throws Exception {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doEncryption(new byte[0]), is(new byte[0]));
}
@Test
public void testDecryptionWhenInputValueIsNull() throws Exception {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doDecryption(null), is(new byte[0]));
}
@Test
public void testDecryptionWhenInputValueIsEmpty() throws Exception {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
assertThat(encryptor.doDecryption(new byte[0]), is(new byte[0]));
}
@Test
public void testEncryption() throws Exception {
for (int i = 0; i < 10; i++) {
final PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(passphrase);
final byte[] plainData = randomUUID().toString().getBytes(UTF_8);
final byte[] cipheredData = encryptor.doEncryption(plainData);
final byte[] decryptedData = encryptor.doDecryption(cipheredData);
final String plainDataAsString = toBase64String(plainData);
final String cipheredDataAsString = toBase64String(cipheredData);
final String decryptedDataAsString = toBase64String(decryptedData);
System.out.printf("plainData=%s, cipheredData=%s %n", plainDataAsString, cipheredDataAsString);
assertThat(plainDataAsString, not(cipheredDataAsString));
assertThat(plainDataAsString, is(decryptedDataAsString));
}
}
private String toBase64String(final byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment