Skip to content

Commit dba38a9

Browse files
Classe de utilidades (#29)
* feat: Classe utilitária para operações de serialização e deserialização JSON. * feat: Classe utilitária para operações de serialização e deserialização JSON. * feat: Classe utilitária para operações de serialização e deserialização JSON. * feat: Classe utilitária para operações de serialização e deserialização JSON. * feat: Classe utilitária para operações de serialização e deserialização JSON. * feat: Classe utilitária para operações de criptografia, descriptografia e hash.
1 parent 0657b19 commit dba38a9

File tree

4 files changed

+792
-0
lines changed

4 files changed

+792
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package br.com.springstudiouslabaws.labcore.exceptions;
2+
3+
public class CryptographyException extends RuntimeException {
4+
5+
public CryptographyException(String message) {
6+
super(message);
7+
}
8+
9+
public CryptographyException(String message, Throwable cause) {
10+
super(message, cause);
11+
}
12+
}
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
package br.com.springstudiouslabaws.labcore.utils;
2+
3+
import br.com.springstudiouslabaws.labcore.exceptions.CryptographyException;
4+
5+
import javax.crypto.Cipher;
6+
import javax.crypto.SecretKey;
7+
import javax.crypto.SecretKeyFactory;
8+
import javax.crypto.spec.GCMParameterSpec;
9+
import javax.crypto.spec.PBEKeySpec;
10+
import javax.crypto.spec.SecretKeySpec;
11+
import java.nio.charset.StandardCharsets;
12+
import java.security.MessageDigest;
13+
import java.security.SecureRandom;
14+
import java.util.Arrays;
15+
import java.util.Base64;
16+
import java.util.logging.Level;
17+
import java.util.logging.Logger;
18+
19+
/**
20+
* Classe utilitária para operações de criptografia, descriptografia e hash.
21+
* Fornece métodos seguros para criptografar dados sensíveis, gerar e verificar hashes,
22+
* utilizando algoritmos criptográficos robustos e práticas de segurança recomendadas.
23+
*
24+
* <p>Esta classe utiliza:
25+
* <ul>
26+
* <li>AES/GCM/NoPadding para criptografia simétrica</li>
27+
* <li>PBKDF2WithHmacSHA256 para derivação de chave</li>
28+
* <li>SecureRandom para geração de IVs e salts</li>
29+
* <li>Limpeza automática de dados sensíveis da memória</li>
30+
* </ul>
31+
*
32+
* <p><strong>Características de Segurança:</strong>
33+
* <ul>
34+
* <li>Utiliza GCM (Galois/Counter Mode) para autenticação e criptografia</li>
35+
* <li>Salt único para cada operação de hash</li>
36+
* <li>IV (vetor de inicialização) único para cada operação de criptografia</li>
37+
* <li>Iterações múltiplas na derivação de chave (65536)</li>
38+
* <li>Chaves de 256 bits</li>
39+
* </ul>
40+
*
41+
* <p><strong>Exemplo de Uso para Criptografia:</strong>
42+
* <pre>{@code
43+
* String dadosSensiveis = "Dados confidenciais";
44+
* String salt = "SaltSeguro123!@#";
45+
*
46+
* // Criptografando dados
47+
* try {
48+
* String dadosCriptografados = CryptographyUtil.encrypt(dadosSensiveis, salt);
49+
* System.out.println("Dados criptografados: " + dadosCriptografados);
50+
*
51+
* // Descriptografando dados
52+
* String dadosOriginais = CryptographyUtil.decrypt(dadosCriptografados, salt);
53+
* System.out.println("Dados descriptografados: " + dadosOriginais);
54+
* } catch (CryptographyException e) {
55+
* // Tratamento de erro apropriado
56+
* logger.error("Erro na operação criptográfica", e);
57+
* }
58+
* }</pre>
59+
*
60+
* <p><strong>Exemplo de Uso para Hash:</strong>
61+
* <pre>{@code
62+
* String senha = "MinhaSenhaSecreta";
63+
*
64+
* // Gerando hash
65+
* try {
66+
* String hashGerado = CryptographyUtil.hash(senha);
67+
*
68+
* // Verificando hash
69+
* boolean hashValido = CryptographyUtil.verifyHash(senha, hashGerado);
70+
* System.out.println("Hash válido: " + hashValido);
71+
* } catch (CryptographyException e) {
72+
* logger.error("Erro na operação de hash", e);
73+
* }
74+
* }</pre>
75+
*
76+
* <p><strong>Considerações de Segurança:</strong>
77+
* <ol>
78+
* <li>O salt deve ser único e seguro para cada contexto de uso</li>
79+
* <li>O salt não precisa ser secreto, mas deve ser armazenado junto com os dados criptografados</li>
80+
* <li>Cada operação de criptografia gera um IV único, garantindo que os mesmos dados gerem
81+
* diferentes resultados criptografados</li>
82+
* <li>Os hashes gerados incluem o salt automaticamente e são seguros para armazenamento de senhas</li>
83+
* </ol>
84+
*
85+
* <p><strong>Exceções:</strong>
86+
* <ul>
87+
* <li>{@link CryptographyException} - Lançada para erros de criptografia, descriptografia,
88+
* hash ou entradas inválidas</li>
89+
* </ul>
90+
*
91+
* @see javax.crypto.Cipher
92+
* @see javax.crypto.SecretKey
93+
* @see java.security.SecureRandom
94+
*/
95+
public final class CryptographyUtil {
96+
private static final Logger LOGGER = Logger.getLogger(CryptographyUtil.class.getName());
97+
98+
private static final String ALGORITHM = "AES/GCM/NoPadding";
99+
private static final int ITERATION_COUNT = 65536;
100+
private static final int KEY_LENGTH = 256;
101+
private static final int GCM_IV_LENGTH = 12;
102+
private static final int GCM_TAG_LENGTH = 16;
103+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
104+
105+
/**
106+
* Construtor privado para prevenir instanciação.
107+
* Esta é uma classe utilitária que não deve ser instanciada.
108+
*
109+
* @throws UnsupportedOperationException sempre, pois a classe não deve ser instanciada
110+
*/
111+
private CryptographyUtil() {
112+
throw new UnsupportedOperationException("Classe utilitária não deve ser instanciada");
113+
}
114+
115+
/**
116+
* Criptografa uma string utilizando AES/GCM/NoPadding.
117+
* O método gera um IV (Vetor de Inicialização) único para cada operação de criptografia,
118+
* garantindo que mesmo dados idênticos produzam resultados diferentes.
119+
*
120+
* <p>O resultado inclui o IV concatenado com o texto cifrado, codificado em Base64.
121+
* Estrutura do resultado: Base64(IV + TextoCifrado)
122+
*
123+
* <p><strong>Exemplo de uso:</strong>
124+
* <pre>{@code
125+
* String dados = "Dados sensíveis";
126+
* String salt = "MeuSaltSecreto123";
127+
*
128+
* try {
129+
* String dadosCriptografados = CryptographyUtil.encrypt(dados, salt);
130+
* // O resultado será diferente a cada chamada devido ao IV único
131+
* } catch (CryptographyException e) {
132+
* // Tratamento do erro
133+
* }
134+
* }</pre>
135+
*
136+
* @param data string a ser criptografada
137+
* @param salt salt usado para derivação da chave
138+
* @return string codificada em Base64 contendo IV + dados criptografados
139+
* @throws CryptographyException se ocorrer erro na criptografia ou se os parâmetros forem inválidos
140+
*/
141+
public static String encrypt(String data, String salt) throws CryptographyException {
142+
validateInputs(data, salt);
143+
try {
144+
byte[] iv = new byte[GCM_IV_LENGTH];
145+
SECURE_RANDOM.nextBytes(iv);
146+
147+
SecretKey key = generateKey(salt);
148+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
149+
150+
Cipher cipher = Cipher.getInstance(ALGORITHM);
151+
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
152+
153+
byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
154+
byte[] combined = new byte[iv.length + cipherText.length];
155+
156+
System.arraycopy(iv, 0, combined, 0, iv.length);
157+
System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
158+
159+
return Base64.getEncoder().encodeToString(combined);
160+
} catch (Exception e) {
161+
LOGGER.log(Level.SEVERE, "Erro na criptografia: {0}", e.getMessage());
162+
throw new CryptographyException("Erro na criptografia dos dados", e);
163+
}
164+
}
165+
166+
167+
/**
168+
* Descriptografa uma string previamente criptografada com {@link #encrypt}.
169+
* O método extrai o IV do início dos dados criptografados e utiliza o salt fornecido
170+
* para derivar a chave de descriptografia.
171+
*
172+
* <p>Se o salt fornecido for diferente do usado na criptografia, ou se os dados
173+
* estiverem corrompidos, uma exceção será lançada.
174+
*
175+
* <p><strong>Exemplo de uso:</strong>
176+
* <pre>{@code
177+
* String dadosCriptografados = "..."; // Dados previamente criptografados
178+
* String salt = "MeuSaltSecreto123"; // Mesmo salt usado na criptografia
179+
*
180+
* try {
181+
* String dadosOriginais = CryptographyUtil.decrypt(dadosCriptografados, salt);
182+
* } catch (CryptographyException e) {
183+
* if (e.getMessage().contains("Falha na autenticação")) {
184+
* // Salt incorreto ou dados corrompidos
185+
* } else {
186+
* // Outro erro de descriptografia
187+
* }
188+
* }
189+
* }</pre>
190+
*
191+
* @param encryptedData dados criptografados em formato Base64(IV + TextoCifrado)
192+
* @param salt salt usado para derivação da chave
193+
* @return string original descriptografada
194+
* @throws CryptographyException se ocorrer erro na descriptografia, autenticação falhar ou parâmetros forem inválidos
195+
*/
196+
public static String decrypt(String encryptedData, String salt) throws CryptographyException {
197+
validateInputs(encryptedData, salt);
198+
try {
199+
byte[] decoded = Base64.getDecoder().decode(encryptedData);
200+
201+
byte[] iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH);
202+
byte[] cipherText = Arrays.copyOfRange(decoded, GCM_IV_LENGTH, decoded.length);
203+
204+
SecretKey key = generateKey(salt);
205+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
206+
207+
Cipher cipher = Cipher.getInstance(ALGORITHM);
208+
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
209+
210+
byte[] decryptedText = cipher.doFinal(cipherText);
211+
return new String(decryptedText, StandardCharsets.UTF_8);
212+
} catch (Exception e) {
213+
String mensagem = e.getMessage();
214+
if (mensagem != null && mensagem.contains("Tag mismatch")) {
215+
// Log em nível INFO para falhas esperadas de autenticação
216+
LOGGER.log(Level.INFO, "Falha na autenticação durante descriptografia: {0}", mensagem);
217+
throw new CryptographyException("Falha na autenticação dos dados criptografados");
218+
} else {
219+
// Mantém log SEVERE para outros erros
220+
LOGGER.log(Level.SEVERE, "Erro na descriptografia: {0}", mensagem);
221+
throw new CryptographyException("Erro na descriptografia dos dados", e);
222+
}
223+
}
224+
}
225+
226+
/**
227+
* Gera um hash seguro para uma string usando PBKDF2WithHmacSHA256.
228+
* O método gera automaticamente um salt aleatório e o inclui no resultado final.
229+
*
230+
* <p>O resultado contém tanto o salt quanto o hash, permitindo verificação posterior
231+
* sem necessidade de armazenar o salt separadamente.
232+
* Estrutura do resultado: Base64(Salt + Hash)
233+
*
234+
* <p><strong>Exemplo de uso:</strong>
235+
* <pre>{@code
236+
* String senha = "SenhaDoUsuario123";
237+
*
238+
* try {
239+
* String hashParaArmazenar = CryptographyUtil.hash(senha);
240+
* // O hash pode ser armazenado diretamente no banco de dados
241+
* } catch (CryptographyException e) {
242+
* // Tratamento do erro
243+
* }
244+
* }</pre>
245+
*
246+
* @param data string para qual será gerado o hash
247+
* @return string em Base64 contendo salt e hash combinados
248+
* @throws CryptographyException se ocorrer erro na geração do hash ou se o parâmetro for inválido
249+
*/
250+
public static String hash(String data) throws CryptographyException {
251+
validateInputs(data);
252+
try {
253+
byte[] salt = generateRandomSalt();
254+
byte[] hash = generateHash(data, salt);
255+
256+
byte[] combined = new byte[salt.length + hash.length];
257+
System.arraycopy(salt, 0, combined, 0, salt.length);
258+
System.arraycopy(hash, 0, combined, salt.length, hash.length);
259+
260+
return Base64.getEncoder().encodeToString(combined);
261+
} catch (Exception e) {
262+
LOGGER.log(Level.SEVERE, "Erro na geração do hash: {0}", e.getMessage());
263+
throw new CryptographyException("Erro na geração do hash", e);
264+
}
265+
}
266+
267+
/**
268+
* Verifica se uma string corresponde a um hash previamente gerado por {@link #hash}.
269+
* O método extrai o salt do hash armazenado e o utiliza para gerar um novo hash
270+
* dos dados fornecidos, comparando-o com o hash armazenado.
271+
*
272+
* <p><strong>Exemplo de uso:</strong>
273+
* <pre>{@code
274+
* String senhaFornecida = "SenhaDoUsuario123";
275+
* String hashArmazenado = "..."; // Hash previamente gerado e armazenado
276+
*
277+
* try {
278+
* boolean senhaCorreta = CryptographyUtil.verifyHash(senhaFornecida, hashArmazenado);
279+
* if (senhaCorreta) {
280+
* // Senha válida
281+
* } else {
282+
* // Senha inválida
283+
* }
284+
* } catch (CryptographyException e) {
285+
* // Erro na verificação (formato inválido, etc)
286+
* }
287+
* }</pre>
288+
*
289+
* @param data string a ser verificada
290+
* @param storedHash hash previamente gerado pelo método {@link #hash}
291+
* @return true se os dados corresponderem ao hash, false caso contrário
292+
* @throws CryptographyException se ocorrer erro na verificação ou se os parâmetros forem inválidos
293+
*/
294+
public static boolean verifyHash(String data, String storedHash) throws CryptographyException {
295+
validateInputs(data, storedHash);
296+
try {
297+
byte[] combined = Base64.getDecoder().decode(storedHash);
298+
299+
byte[] salt = Arrays.copyOfRange(combined, 0, 16);
300+
byte[] hash = Arrays.copyOfRange(combined, 16, combined.length);
301+
302+
byte[] testHash = generateHash(data, salt);
303+
return MessageDigest.isEqual(hash, testHash);
304+
} catch (Exception e) {
305+
LOGGER.log(Level.SEVERE, "Erro na verificação do hash: {0}", e.getMessage());
306+
throw new CryptographyException("Erro na verificação do hash", e);
307+
}
308+
}
309+
310+
/**
311+
* Gera uma chave secreta a partir do salt fornecido usando PBKDF2WithHmacSHA256.
312+
*
313+
* @param salt salt para derivação da chave
314+
* @return chave secreta gerada
315+
* @throws Exception se ocorrer erro na geração da chave
316+
*/
317+
private static SecretKey generateKey(String salt) throws Exception {
318+
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
319+
PBEKeySpec spec = new PBEKeySpec(
320+
salt.toCharArray(),
321+
salt.getBytes(StandardCharsets.UTF_8),
322+
ITERATION_COUNT,
323+
KEY_LENGTH
324+
);
325+
326+
try {
327+
SecretKey tmp = factory.generateSecret(spec);
328+
return new SecretKeySpec(tmp.getEncoded(), "AES");
329+
} finally {
330+
Arrays.fill(spec.getPassword(), '\u0000'); // Limpa dados sensíveis
331+
}
332+
}
333+
334+
/**
335+
* Gera um salt aleatório de 16 bytes usando SecureRandom.
336+
*
337+
* @return array de bytes contendo o salt gerado
338+
*/
339+
private static byte[] generateRandomSalt() {
340+
byte[] salt = new byte[16];
341+
SECURE_RANDOM.nextBytes(salt);
342+
return salt;
343+
}
344+
345+
/**
346+
* Gera um hash PBKDF2 com os parâmetros fornecidos.
347+
*
348+
* @param data dados para gerar o hash
349+
* @param salt salt a ser usado na geração do hash
350+
* @return array de bytes contendo o hash gerado
351+
* @throws Exception se ocorrer erro na geração do hash
352+
*/
353+
private static byte[] generateHash(String data, byte[] salt) throws Exception {
354+
PBEKeySpec spec = new PBEKeySpec(
355+
data.toCharArray(),
356+
salt,
357+
ITERATION_COUNT,
358+
KEY_LENGTH
359+
);
360+
try {
361+
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
362+
return skf.generateSecret(spec).getEncoded();
363+
} finally {
364+
Arrays.fill(spec.getPassword(), '\u0000'); // Limpa dados sensíveis
365+
}
366+
}
367+
368+
/**
369+
* Valida as entradas fornecidas, garantindo que não sejam nulas ou vazias.
370+
*
371+
* @param inputs strings a serem validadas
372+
* @throws CryptographyException se alguma das entradas for nula ou vazia
373+
*/
374+
private static void validateInputs(String... inputs) {
375+
for (String input : inputs) {
376+
if (input == null || input.isEmpty()) {
377+
throw new CryptographyException("Parâmetro de entrada não pode ser nulo ou vazio");
378+
}
379+
}
380+
}
381+
}

0 commit comments

Comments
 (0)