Recentemente tive de implementar um método de criptografia AES no Java de forma a ser compatível com a criptografia implementada em C#
no back-end do cliente.
O código implementado em C#
pelo cliente era análogo à este:
public static string Aes_EncryptString(string toEncrypt)
{
try
{
string encrypted = null;
AesManaged aes = null;
try
{
var password = "hswPJNdopw5as0NJqweVCdA1FKsNYIlkdoVlk/poifW=";
byte[] salt = Encoding.ASCII.GetBytes("ps85hjk98ng23ga");
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt);
aes = new AesManaged();
aes.Key = key.GetBytes(aes.KeySize / 8);
aes.IV = key.GetBytes(aes.BlockSize / 8);
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream memory = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(memory, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(toEncrypt);
}
}
encrypted = Convert.ToBase64String(memory.ToArray());
}
}
finally
{
if (aes != null) aes.Clear();
}
return encrypted;
}
catch (Exception ex)
{
throw ex;
}
}
Pois bem tentei usar as classes de criptografia do Java mas nunca conseguia obter os mesmos resultados que o algoritmo escrito em C#
.
O problema é que a implementação do PBEKeySpec
do Java
só suporta vetores de inicialização aleatórios e não pseudo-aleatórios como o C#
, dessa forma nunca era possível obter a mesma encriptação como na implementação feita em C#
.
A solução foi criar uma implementação do Rfc2898DeriveBytes
em Java
.
Essa implementação eu fiz como inner class de uma classe chamada AesCipher
, a qual segue o código:
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Algoritmo de enciptação/decriptação AES compatível com C# .Net.
*
* @author ricardo.staroski
*/
public final class AesCipher {
/**
* Derivação de senha RFC 2898.<br>
* Compatível com a classe <code>Rfc2898DeriveBytes</code> do C# .Net.
*/
private static final class Rfc2898DeriveBytes {
/**
* Converte um <code>int</code> em um array de 4 <code>byte</code>s.
*
* @param value O valor <code>int</code>.
* @return Os 4 <code>byte</code>s que compõe o <code>int</code> informado.
*/
private static byte[] intToBytes(int value) {
return new byte[] { (byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) (value >>> 0) };
}
private Mac hmacSha1;
private byte[] salt;
private byte[] buffer = new byte[20];
private int iterationCount;
private int bufferStartIndex = 0;
private int bufferEndIndex = 0;
private int block = 1;
/**
* @param password A senha utilizada para derivar a chave.
* @param salt O salto utilizado para derivar a chave.
* @param iterations O número de iterações da operação.
* @throws NoSuchAlgorithmException Se o algoritmo HmacSHA1 núo estiver disponúvel.
* @throws InvalidKeyException Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
*/
public Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeyException {
if ((salt == null) || (salt.length < 8)) {
throw new InvalidKeyException("Salt must be 8 bytes or more.");
}
if (password == null) {
throw new InvalidKeyException("Password cannot be null.");
}
this.salt = salt;
this.iterationCount = iterations;
this.hmacSha1 = Mac.getInstance("HmacSHA1");
this.hmacSha1.init(new SecretKeySpec(password, "HmacSHA1"));
}
/**
* @param password A senha utilizada para derivar a chave.
* @param salt O salto utilizado para derivar a chave.
* @throws NoSuchAlgorithmException Se o algoritmo HmacSHA1 não estiver disponúvel.
* @throws InvalidKeyException Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
* @throws UnsupportedEncodingException Se o encoding UTF-8 não for suportado.
*/
public Rfc2898DeriveBytes(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
this(password, salt, 1000);
}
/**
* @param password A senha utilizada para derivar a chave.
* @param salt O salto utilizado para derivar a chave.
* @param iterations O número de iteraçães da operação.
* @throws NoSuchAlgorithmException Se o algoritmo HmacSHA1 não estiver disponível.
* @throws InvalidKeyException Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
* @throws UnsupportedEncodingException Se o encoding UTF-8 não for suportado.
*/
public Rfc2898DeriveBytes(String password, byte[] salt, int iterations) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
this(password.getBytes("UTF8"), salt, iterations);
}
/**
* Retorna uma chave pseudo-aleatória de uma senha, salto e iterações.
*
* @param count Número de bytes para retornar.
* @return O array de bytes.
*/
public byte[] getBytes(int count) {
byte[] result = new byte[count];
int resultOffset = 0;
int bufferCount = this.bufferEndIndex - this.bufferStartIndex;
if (bufferCount > 0) {
if (count < bufferCount) {
System.arraycopy(this.buffer, this.bufferStartIndex, result, 0, count);
this.bufferStartIndex += count;
return result;
}
System.arraycopy(this.buffer, this.bufferStartIndex, result, 0, bufferCount);
this.bufferStartIndex = this.bufferEndIndex = 0;
resultOffset += bufferCount;
}
while (resultOffset < count) {
int needCount = count - resultOffset;
this.buffer = this.func();
if (needCount > 20) {
System.arraycopy(this.buffer, 0, result, resultOffset, 20);
resultOffset += 20;
} else {
System.arraycopy(this.buffer, 0, result, resultOffset, needCount);
this.bufferStartIndex = needCount;
this.bufferEndIndex = 20;
return result;
}
}
return result;
}
private byte[] func() {
this.hmacSha1.update(this.salt, 0, this.salt.length);
byte[] tempHash = this.hmacSha1.doFinal(intToBytes(this.block));
this.hmacSha1.reset();
byte[] finalHash = tempHash;
for (int i = 2; i <= this.iterationCount; i++) {
tempHash = this.hmacSha1.doFinal(tempHash);
for (int j = 0; j < 20; j++) {
finalHash[j] = (byte) (finalHash[j] ^ tempHash[j]);
}
}
if (this.block == 2147483647) {
this.block = -2147483648;
} else {
this.block += 1;
}
return finalHash;
}
}
private final SecretKeySpec key;
private final IvParameterSpec iv;
private final Cipher aesAlgorithm;
/**
* Construtor parametrizado.
*
* @param password A senha a ser utilizada.
* @param salt O salto a ser utilizado.
*/
public AesCipher(String password, String salt) {
this(password, salt.getBytes());
}
/**
* Construtor parametrizado.
*
* @param password A senha a ser utilizada.
* @param salt O salto a ser utilizado.
*/
public AesCipher(String password, byte[] salt) {
try {
aesAlgorithm = Cipher.getInstance("AES/CBC/PKCS5Padding");
Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, salt);
key = new SecretKeySpec(deriveBytes.getBytes(256 / 8), "AES");
iv = new IvParameterSpec(deriveBytes.getBytes(128 / 8));
} catch (Exception e) {
throw new RuntimeException(e.getLocalizedMessage(), e);
}
}
/**
* Decripta a {@link String} informada.
*
* @param toDecrypt {@link String} a ser decriptada.
* @return A {@link String} decriptada.
*/
public String decrypt(String toDecrypt) {
try {
aesAlgorithm.init(Cipher.DECRYPT_MODE, key, iv);
byte[] encoded = toDecrypt.getBytes();
byte[] encrypted = Base64.getDecoder().decode(encoded);
byte[] decrypted = aesAlgorithm.doFinal(encrypted);
return new String(decrypted);
} catch (Exception e) {
throw new RuntimeException(e.getLocalizedMessage(), e);
}
}
/**
* Encripta a {@link String} informada.
*
* @param toEncrypt {@link String} a ser encriptada.
* @return A {@link String} encriptada.
*/
public String encrypt(String toEncrypt) {
try {
aesAlgorithm.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] decrypted = toEncrypt.getBytes();
byte[] encrypted = aesAlgorithm.doFinal(decrypted);
byte[] encoded = Base64.getEncoder().encode(encrypted);
return new String(encoded);
} catch (Exception e) {
throw new RuntimeException(e.getLocalizedMessage(), e);
}
}
}
Com a classe classe AesCipher
consegui reproduzir a funcionalidade do código C#
da seguinte forma no Java
:
public static String aesEncryptString(String toEncrypt) {
String password = "hswPJNdopw5as0NJqweVCdA1FKsNYIlkdoVlk/poifW=";
byte[] salt = "ps85hjk98ng23ga".getBytes();
AesCipher aes = new AesCipher(password, salt);
String encrypted = aes.encrypt(toEncrypt);
return encrypted;
}
Como essa implementação me trouxe uma certa dor de cabeça, resolvi compartilhá-la com a comunidade, caso alguém passe pelo mesmo problema futuramente.