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