1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| public class CookieUtil {
private static final int IV_LEN = 16;
private static final int HMAC_LEN = 32;
private final byte[] encKey; // AES-256 키
private final byte[] hmacKey; // HMAC 키
// 쿠키 생성
public void addSecureCookie(HttpServletResponse resp,
String name,
String plaintext,
int maxAgeSec) throws Exception {
byte[] iv = new byte[IV_LEN];
new SecureRandom().nextBytes(iv);
// AES-256-CBC 암호화
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(encKey, "AES"),
new IvParameterSpec(iv));
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
// HMAC 생성
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(hmacKey, "HmacSHA256"));
mac.update(iv);
mac.update(ciphertext);
byte[] hmac = mac.doFinal();
// payload = IV + HMAC + 암호문
byte[] payload = new byte[iv.length + hmac.length + ciphertext.length];
System.arraycopy(iv, 0, payload, 0, iv.length);
System.arraycopy(hmac, 0, payload, iv.length, hmac.length);
System.arraycopy(ciphertext, 0, payload, iv.length + hmac.length, ciphertext.length);
String encoded = Base64.getUrlEncoder().withoutPadding()
.encodeToString(payload);
// HttpOnly, Secure, SameSite 속성 설정
String header = String.format(
"%s=%s; Max-Age=%d; Path=/; HttpOnly; Secure; SameSite=Strict",
name, encoded, maxAgeSec
);
resp.addHeader("Set-Cookie", header);
}
// 쿠키 읽기 (HMAC 검증 + 복호화)
public String readSecureCookie(String value) throws Exception {
if (value == null) return null;
byte[] data = Base64.getUrlDecoder().decode(value);
byte[] iv = Arrays.copyOfRange(data, 0, IV_LEN);
byte[] hmac = Arrays.copyOfRange(data, IV_LEN, IV_LEN + HMAC_LEN);
byte[] ciphertext = Arrays.copyOfRange(data, IV_LEN + HMAC_LEN, data.length);
// 복호화 전 HMAC 검증
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(hmacKey, "HmacSHA256"));
mac.update(iv);
mac.update(ciphertext);
byte[] calc = mac.doFinal();
if (!MessageDigest.isEqual(hmac, calc)) {
return null; // 변조 탐지
}
// 복호화
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(encKey, "AES"),
new IvParameterSpec(iv));
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
}
}
|