Last active
June 19, 2016 22:33
-
-
Save garcia-jj/ee178bcf5a369c8cca48 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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