From 22204a48a1444b068a43ea8001fb05a4e081520d Mon Sep 17 00:00:00 2001 From: tech Date: Tue, 21 Apr 2026 10:32:46 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20AES-256-GCM=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94=20=EC=A0=81=EC=9A=A9=20(tel,=20email),=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8/=EB=B0=B0=ED=8F=AC=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/owrawww/service/CareersService.java | 6 +- .../com/owrawww/service/InquiryService.java | 6 +- src/main/java/com/owrawww/util/AesUtil.java | 78 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 17 ++-- src/main/resources/application-prod.yml | 5 +- src/main/resources/application.yml | 2 + .../resources/templates/include/footer.html | 8 +- 7 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/owrawww/util/AesUtil.java diff --git a/src/main/java/com/owrawww/service/CareersService.java b/src/main/java/com/owrawww/service/CareersService.java index c8c6b8f..43aeec1 100644 --- a/src/main/java/com/owrawww/service/CareersService.java +++ b/src/main/java/com/owrawww/service/CareersService.java @@ -2,6 +2,7 @@ package com.owrawww.service; import com.owrawww.domain.Careers; import com.owrawww.domain.mapper.CareersMapper; +import com.owrawww.util.AesUtil; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ public class CareersService { private String uploadPath; private final CareersMapper careersMapper; + private final AesUtil aesUtil; public boolean submit(Careers careers, MultipartFile file) throws IOException { Careers saved = new Careers(); @@ -29,8 +31,8 @@ public class CareersService { saved.setTitle(careers.getTitle()); saved.setComment(careers.getContent()); saved.setName(careers.getName()); - saved.setTel(careers.getTel()); - saved.setEmail(careers.getEmail()); + saved.setTel(aesUtil.encrypt(careers.getTel())); + saved.setEmail(aesUtil.encrypt(careers.getEmail())); saved.setTopCode(2); saved.setLeftCode(1); saved.setSubGubun(1); diff --git a/src/main/java/com/owrawww/service/InquiryService.java b/src/main/java/com/owrawww/service/InquiryService.java index 283c821..c83fb6f 100644 --- a/src/main/java/com/owrawww/service/InquiryService.java +++ b/src/main/java/com/owrawww/service/InquiryService.java @@ -2,6 +2,7 @@ package com.owrawww.service; import com.owrawww.domain.Inquiry; import com.owrawww.domain.mapper.InquiryMapper; +import com.owrawww.util.AesUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,6 +11,7 @@ import org.springframework.stereotype.Service; public class InquiryService { private final InquiryMapper inquiryMapper; + private final AesUtil aesUtil; public boolean submit(Inquiry inquiry) { Inquiry savedInquiry = new Inquiry(); @@ -17,8 +19,8 @@ public class InquiryService { savedInquiry.setTitle(inquiry.getTitle()); savedInquiry.setComment(inquiry.getContent()); savedInquiry.setName(inquiry.getName()); - savedInquiry.setTel(inquiry.getTel()); - savedInquiry.setEmail(inquiry.getEmail()); + savedInquiry.setTel(aesUtil.encrypt(inquiry.getTel())); + savedInquiry.setEmail(aesUtil.encrypt(inquiry.getEmail())); savedInquiry.setTopCode(2); savedInquiry.setLeftCode(1); savedInquiry.setSubGubun(1); diff --git a/src/main/java/com/owrawww/util/AesUtil.java b/src/main/java/com/owrawww/util/AesUtil.java new file mode 100644 index 0000000..6931f65 --- /dev/null +++ b/src/main/java/com/owrawww/util/AesUtil.java @@ -0,0 +1,78 @@ +package com.owrawww.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * AES-256-GCM 암호화/복호화 유틸리티 + * - IV(12 bytes) + 암호문을 Base64로 인코딩하여 저장 + */ +@Component +public class AesUtil { + + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final int IV_LENGTH = 12; // GCM 권장 IV 크기 + private static final int TAG_BIT_LEN = 128; // GCM 인증 태그 크기 + + private final SecretKey secretKey; + + public AesUtil(@Value("${app.encryption.key}") String base64Key) { + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + if (keyBytes.length != 32) { + throw new IllegalArgumentException("암호화 키는 Base64 인코딩된 32바이트(256bit)여야 합니다."); + } + this.secretKey = new SecretKeySpec(keyBytes, "AES"); + } + + /** + * 평문 → AES-256-GCM 암호화 → Base64 문자열 + */ + public String encrypt(String plainText) { + if (plainText == null || plainText.isEmpty()) return plainText; + try { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_BIT_LEN, iv)); + byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8")); + + // IV + 암호문 결합 후 Base64 인코딩 + byte[] combined = new byte[IV_LENGTH + encrypted.length]; + System.arraycopy(iv, 0, combined, 0, IV_LENGTH); + System.arraycopy(encrypted, 0, combined, IV_LENGTH, encrypted.length); + + return Base64.getEncoder().encodeToString(combined); + } catch (Exception e) { + throw new RuntimeException("암호화 실패", e); + } + } + + /** + * Base64 암호문 → AES-256-GCM 복호화 → 평문 + */ + public String decrypt(String encryptedText) { + if (encryptedText == null || encryptedText.isEmpty()) return encryptedText; + try { + byte[] combined = Base64.getDecoder().decode(encryptedText); + byte[] iv = new byte[IV_LENGTH]; + byte[] cipherText = new byte[combined.length - IV_LENGTH]; + + System.arraycopy(combined, 0, iv, 0, IV_LENGTH); + System.arraycopy(combined, IV_LENGTH, cipherText, 0, cipherText.length); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_BIT_LEN, iv)); + return new String(cipher.doFinal(cipherText), "UTF-8"); + } catch (Exception e) { + throw new RuntimeException("복호화 실패", e); + } + } +} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index c192c9b..e045d8e 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,5 +1,12 @@ -{"properties": [{ - "name": "app.upload.path", - "type": "java.lang.String", - "description": "A description for 'app.upload.path'" -}]} \ No newline at end of file +{"properties": [ + { + "name": "app.upload.path", + "type": "java.lang.String", + "description": "A description for 'app.upload.path'" + }, + { + "name": "app.encryption.key", + "type": "java.lang.String", + "description": "A description for 'app.encryption.key'" + } +]} \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 0966505..e4df70a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -28,6 +28,9 @@ logging: max-history: 30 file-name-pattern: /home/www/owrainfo/logs/owrawww.%d{yyyy-MM-dd}.%i.log -app: +app: upload: path: /home/www/owrainfo/uploads + encryption: + key: PIYAsB81yr7uETVoM/E7Sz9Sp5kNB46uHyjnfQE3XOs= # 운영 전용 키로 교체 필요 (base64 인코딩된 32바이트) + \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3ff513e..41551f6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,3 +30,5 @@ server: app: upload: path: D:/uploads/owrawww/careers # 기본값 (프로파일별로 override) + encryption: + key: PIYAsB81yr7uETVoM/E7Sz9Sp5kNB46uHyjnfQE3XOs= # AES-256 키 (운영에서는 반드시 별도 키로 교체) diff --git a/src/main/resources/templates/include/footer.html b/src/main/resources/templates/include/footer.html index 65b2a89..a1e8a50 100644 --- a/src/main/resources/templates/include/footer.html +++ b/src/main/resources/templates/include/footer.html @@ -1,10 +1,10 @@