Implementando Criptografia de Dados em Comunicações Vue.js e Spring Boot

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);
        }
    }
}

Tags: Vue.js Spring Boot AES Encryption Axios Interceptors RequestBodyAdvice

Publicado em 7-31 03:19