Visão Geral da Criptografia de Dados em Aplicações Web
Em sistemas web modernos, a segurança da informação é primordial. A transmissão de dados sensíveis entre o frontend e o backend requer mecanismos robustos para proteger a confidencialidade e a integridade. Este artigo detalha uma abordagem prática para implementar a criptografia de dados em requisições e respostas HTTP utilizando Vue.js no lado do cliente e Spring Boot no lado do servidor, empregando o algoritmo AES no modo CBC com preenchimento ZeroPadding.
É importante notar que a solução aqui apresentada foca primariamente na criptografia de requisições que possuem um corpo (payload), como operações POST, PUT ou PATCH. Requisições sem corpo, tipicamente GET, não serão alvo de criptografia de dados de requisição nesta implementação.
Implementação Frontend com Vue.js
A seguir, os passos para configurar a criptografia e descriptografia no lado do cliente, utilizando a biblioteca crypto-js e interceptores Axios.
1. Configuração e Funções de Criptografia/Descriptografia
Primeiramente, instale a biblioteca crypto-js, se ainda não o fez:
npm install crypto-js --save-dev
Crie um arquivo de utilidade (por exemplo, src/utils/cryptoService.ts) para encapsular as funções de criptografia AES. As chaves de criptografia e vetores de inicialização (IV) devem ser mantidos em sincronia com o backend.
import CryptoJS from 'crypto-js';
// Chave e IV devem ser idênticos aos utilizados no backend
const SECRET_KEY_STR = '1234567890123456'; // 16 bytes (128 bits)
const INITIAL_VECTOR_STR = 'ABCDEFG123456789'; // 16 bytes (128 bits)
const parsedKey = CryptoJS.enc.Utf8.parse(SECRET_KEY_STR);
const parsedIv = CryptoJS.enc.Utf8.parse(INITIAL_VECTOR_STR);
/**
* Criptografa uma string usando AES no modo CBC com ZeroPadding.
* @param {string} dataString - A string a ser criptografada.
* @returns {string} Dados criptografados em formato Base64.
*/
export function encryptData(dataString: string): string {
const dataSource = CryptoJS.enc.Utf8.parse(dataString);
const encrypted = CryptoJS.AES.encrypt(dataSource, parsedKey, {
iv: parsedIv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
// O ciphertext precisa ser convertido para Base64 para transmissão
return encrypted.toString();
}
/**
* Descriptografa uma string criptografada em Base64.
* @param {string} encryptedBase64Str - A string criptografada em Base64.
* @returns {string} A string original descriptografada.
*/
export function decryptData(encryptedBase64Str: string): string {
const decrypted = CryptoJS.AES.decrypt(encryptedBase64Str, parsedKey, {
iv: parsedIv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
// Exporta as funções para serem usadas em outros módulos
export default {
encryptData,
decryptData
};
2. Integração com Interceptores Axios
No arquivo onde sua instância Axios é configurada, importe as funções de criptografia e descriptografia.
import axios, { AxiosResponse } from 'axios';
import cryptoService from '@/utils/cryptoService'; // Ajuste o caminho conforme necessário
const httpClient = axios.create({
baseURL: '/api', // Seu base URL
headers: {
'Content-Type': 'application/json;charset=UTF-8' // Importante para consistência de codificação
}
});
Interceptor de Requisição (Pré-envio)
Configure um interceptor para criptografar o corpo da requisição antes que ela seja enviada. Este interceptor só agirá em requisições que possuem um corpo de dados.
httpClient.interceptors.request.use(
(config) => {
if (config.data) {
// Converte o objeto de dados para JSON string antes de criptografar
const jsonData = JSON.stringify(config.data);
config.data = cryptoService.encryptData(jsonData);
}
return config;
},
(error) => {
// Lidar com erros de requisição
console.error('Erro no interceptor de requisição:', error);
return Promise.reject(error);
}
);
Interceptor de Resposta (Pós-recebimento)
Configure um interceptor para descriptografar os dados da resposta recebida do servidor.
httpClient.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status === 200 && response.data) {
// Remove espaços e quebras de linha para garantir a decodificação correta
const cleanedData = response.data.replace(/\s/g, '');
const decryptedString = cryptoService.decryptData(cleanedData);
// Parseia a string JSON descriptografada de volta para um objeto
response.data = JSON.parse(decryptedString);
}
return response;
},
(error) => {
// Lidar com erros de resposta
console.error('Erro no interceptor de resposta:', error);
return Promise.reject(error);
}
);
Implementação Backend com Spring Boot
No backend Spring Boot, utilizaremos RequestBodyAdvice e ResponseBodyAdvice para interceptar e processar os corpos das requisições e respostas de forma centralizada.
1. Classe de Utilidade de Criptografia
Crie uma classe de utilidade (por exemplo, com.example.util.CryptoUtil.java) que implementa as funções AES de criptografia e descriptografia. É crucial que a chave e o IV sejam idênticos aos usados no frontend, e que o esquema de padding seja compatível.
package com.example.util;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class CryptoUtil {
private static final String APP_SECRET_KEY = "1234567890123456"; // 16 bytes
private static final String APP_IV_PARAM = "ABCDEFG123456789"; // 16 bytes
private static SecretKeySpec getKeySpec() {
return new SecretKeySpec(APP_SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
}
private static IvParameterSpec getIvSpec() {
return new IvParameterSpec(APP_IV_PARAM.getBytes(StandardCharsets.UTF_8));
}
/**
* Criptografa uma string e retorna o resultado em Base64.
* O padding ZeroPadding do JS é simulado com padding manual e NoPadding no Java.
* @param plainText A string a ser criptografada.
* @return String criptografada em Base64.
*/
public static String encryptString(String plainText) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = plainText.getBytes(StandardCharsets.UTF_8);
// Simula ZeroPadding: Preenche o array de bytes para ser múltiplo do tamanho do bloco
int paddedLength = dataBytes.length;
if (paddedLength % blockSize != 0) {
paddedLength = paddedLength + (blockSize - (paddedLength % blockSize));
}
byte[] paddedData = new byte[paddedLength];
System.arraycopy(dataBytes, 0, paddedData, 0, dataBytes.length);
cipher.init(Cipher.ENCRYPT_MODE, getKeySpec(), getIvSpec());
byte[] encryptedBytes = cipher.doFinal(paddedData);
return Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Descriptografa uma string Base64 e retorna os bytes originais.
* @param encryptedBase64 A string Base64 criptografada.
* @return Array de bytes descriptografados.
*/
public static byte[] decryptBytes(String encryptedBase64) {
try {
byte[] cipherTextBytes = Base64.decodeBase64(encryptedBase64);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, getKeySpec(), getIvSpec());
byte[] decryptedBytesWithPadding = cipher.doFinal(cipherTextBytes);
// Remove o ZeroPadding manual (encontra o primeiro byte nulo a partir do fim)
int i = decryptedBytesWithPadding.length - 1;
while (i >= 0 && decryptedBytesWithPadding[i] == 0) {
i--;
}
return Arrays.copyOf(decryptedBytesWithPadding, i + 1);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2. Interceptor de Requisição para Descriptografia (RequestBodyAdvice)
Crie uma classe que implemente RequestBodyAdvice para interceptar e descriptografar o corpo das requisições antes que o Spring o processe. Anote-a com @ControllerAdvice.
package com.example.config;
import com.example.util.CryptoUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
@ControllerAdvice
public class RequestDecryptionAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// Retorna true para aplicar este advice a todos os @RequestBody
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
// Envolve a mensagem de entrada com nossa implementação personalizada de descriptografia
return new DecryptedInputMessageWrapper(inputMessage);
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body; // Não é necessário fazer nada após a leitura do corpo.
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body; // Não é necessário fazer nada para corpo vazio.
}
}
// Classe wrapper para interceptar e descriptografar o InputStream da requisição
class DecryptedInputMessageWrapper implements HttpInputMessage {
private final HttpInputMessage originalMessage;
private final InputStream decryptedBody;
public DecryptedInputMessageWrapper(HttpInputMessage originalMessage) throws IOException {
this.originalMessage = originalMessage;
String encryptedContent = new String(originalMessage.getBody().readAllBytes(), StandardCharsets.UTF_8);
byte[] decryptedBytes = CryptoUtil.decryptBytes(encryptedContent);
this.decryptedBody = new ByteArrayInputStream(decryptedBytes);
}
@Override
public InputStream getBody() {
return decryptedBody;
}
@Override
public HttpHeaders getHeaders() {
return originalMessage.getHeaders();
}
}
3. Interceptor de Resposta para Criptografia (ResponseBodyAdvice)
Crie uma classe que implemente ResponseBodyAdvice para interceptar e criptografar o corpo das respostas antes que elas sejam enviadas ao cliente. Anote-a com @ControllerAdvice.
package com.example.config;
import com.example.util.CryptoUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.io.IOException;
@ControllerAdvice
public class ResponseEncryptionAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// Retorna true para aplicar este advice a todas as respostas.
// Você pode adicionar lógica condicional aqui, por exemplo,
// usando uma anotação personalizada para ativar/desativar.
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
return null; // Não há corpo para criptografar
}
try {
// Converte o objeto de resposta para uma string JSON
String jsonResponse = objectMapper.writeValueAsString(body);
// Criptografa a string JSON
return CryptoUtil.encryptString(jsonResponse);
} catch (JsonProcessingException e) {
System.err.println("Erro ao serializar ou criptografar resposta: " + e.getMessage());
// Em caso de erro, você pode optar por lançar uma exceção,
// retornar o corpo original ou um corpo de erro criptografado.
throw new RuntimeException("Falha na criptografia da resposta", e);
}
}
}