Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.example.cs25service.domain.quiz.controller;

import com.example.cs25common.global.dto.ApiResponse;
import com.example.cs25service.domain.quiz.dto.TodayQuizResponseDto;
import com.example.cs25service.domain.quiz.service.QuizAccuracyCalculateService;
import com.example.cs25service.domain.quiz.service.QuizPageService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class QuizTestController {

private final QuizAccuracyCalculateService accuracyService;
private final QuizPageService quizPageService;

@GetMapping("/accuracyTest")
public ApiResponse<Void> accuracyTest() {
Expand All @@ -27,6 +32,17 @@ public ApiResponse<Void> accuracyTest() {
// return new ApiResponse<>(200);
// }

// @GetMapping("/test/todayQuiz")
// public ApiResponse<TodayQuizResponseDto> showTodayQuizPage(
// @RequestParam("quizId") String quizId
// ) {
//
// return new ApiResponse<>(
// 200,
// quizPageService.showTodayQuizPage(quizId)
// );
// }

// @GetMapping("/test/sse")
// public void testSse(HttpServletResponse response) throws IOException {
// response.setContentType("text/event-stream");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.example.cs25service.domain.quiz.dto.TodayQuizResponseDto;
import java.util.Arrays;
import java.util.List;

import com.example.cs25service.domain.quiz.util.AesUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,7 +20,7 @@
public class QuizPageService {

private final QuizRepository quizRepository;

private final AesUtil aesUtil;
/**
* 오늘의 문제를 반환해주는 메서드
* @param quizId 문제 id
Expand Down Expand Up @@ -56,8 +58,8 @@ private TodayQuizResponseDto getMultipleQuiz(Quiz quiz) {
.choice2(choices.get(1))
.choice3(choices.get(2))
.choice4(choices.get(3))
.answerNumber(answerNumber)
.commentary(quiz.getCommentary())
.answerNumber(aesUtil.encrypt(answerNumber))
.commentary(aesUtil.encrypt(quiz.getCommentary()))
.quizType(quiz.getType().name())
Comment on lines +61 to 63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

암호화로 인해 기존 응답 스키마가 깨집니다

Line 61과 Line 79에서 바로 암호화된 값을 내려보내면 /todayQuiz 실제 엔드포인트를 소비하는 기존 프런트가 즉시 깨집니다(현재 배포본은 복호화 로직이 없음). 프런트 적용이 완료될 때까지는 최소한 기능 플래그나 엔드포인트 버전 분리로 기존 소비자에게 평문을 유지해야 합니다.

Also applies to: 79-80

.quizLevel(quiz.getLevel().name())
.category(getQuizCategory(quiz))
Expand All @@ -74,8 +76,8 @@ private TodayQuizResponseDto getDescriptiveQuiz(Quiz quiz) {
return TodayQuizResponseDto.builder()
.question(quiz.getQuestion())
.quizType(quiz.getQuestion())
.answer(quiz.getAnswer())
.commentary(quiz.getCommentary())
.answer(aesUtil.encrypt(quiz.getAnswer()))
.commentary(aesUtil.encrypt(quiz.getCommentary()))
.quizType(quiz.getType().name())
.quizLevel(quiz.getLevel().name())
.category(getQuizCategory(quiz))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.example.cs25service.domain.quiz.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.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

@Component
public class AesUtil {

private final String secretKey;

public AesUtil(@Value("${aes.secret.key}") String secretKey) {
this.secretKey = secretKey;
}

private final String ALGORITHM = "AES/CBC/PKCS5Padding";

private SecretKey getKey() {
return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
}
Comment on lines +17 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

비정상 AES 키 길이 허용으로 인한 런타임 실패 위험

현재 secretKey를 그대로 보관했다가 SecretKeySpec을 매번 만들고 있습니다. 프로퍼티 값이 16·24·32바이트가 아니거나 UTF-8 멀티바이트 문자를 포함하면 Cipher.init() 단계에서 InvalidKeyException이 발생해 암·복호화가 모두 실패합니다. PR 설명에도 48바이트 키 사용 가능하다고 적혀 있어 실제로 그렇게 설정하면 바로 장애가 납니다. 생성자에서 키 길이를 검증하고, 한 번 생성한 SecretKey를 재사용하도록 수정해 주세요.

-    private final String secretKey;
-
-    public AesUtil(@Value("${aes.secret.key}") String secretKey) {
-        this.secretKey = secretKey;
-    }
-
-    private SecretKey getKey() {
-        return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
-    }
+    private final SecretKey secretKey;
+
+    public AesUtil(@Value("${aes.secret.key}") String secretKey) {
+        byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
+        if (keyBytes.length != 16 && keyBytes.length != 24 && keyBytes.length != 32) {
+            throw new IllegalArgumentException("aes.secret.key는 16, 24 또는 32바이트여야 합니다.");
+        }
+        this.secretKey = new SecretKeySpec(keyBytes, "AES");
+    }
+
+    private SecretKey getKey() {
+        return secretKey;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final String secretKey;
public AesUtil(@Value("${aes.secret.key}") String secretKey) {
this.secretKey = secretKey;
}
private final String ALGORITHM = "AES/CBC/PKCS5Padding";
private SecretKey getKey() {
return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
}
// Replace the String-backed key with a pre-validated, cached SecretKey
private final SecretKey secretKey;
public AesUtil(@Value("${aes.secret.key}") String secretKey) {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
if (keyBytes.length != 16 && keyBytes.length != 24 && keyBytes.length != 32) {
throw new IllegalArgumentException("aes.secret.key는 16, 24 또는 32바이트여야 합니다.");
}
this.secretKey = new SecretKeySpec(keyBytes, "AES");
}
private final String ALGORITHM = "AES/CBC/PKCS5Padding";
private SecretKey getKey() {
return secretKey;
}


/** 암호화 */
public String encrypt(String plainText) {
if (plainText == null) return null;
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);

cipher.init(Cipher.ENCRYPT_MODE, getKey(), ivSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.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("AES encryption error", e);
}
}

/** 복호화 */
public String decrypt(String cipherText) {
if (cipherText == null) return null;
try {
byte[] decoded = Base64.getDecoder().decode(cipherText);

byte[] iv = new byte[16];
byte[] encrypted = new byte[decoded.length - 16];
System.arraycopy(decoded, 0, iv, 0, iv.length);
System.arraycopy(decoded, iv.length, encrypted, 0, encrypted.length);

Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
byte[] original = cipher.doFinal(encrypted);

return new String(original, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES decryption error", e);
}
}
}
4 changes: 3 additions & 1 deletion cs25-service/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,6 @@ server.servlet.session.cookie.secure=true
FRONT_END_URI=https://cs25.co.kr
## JSESSIONID Secure - ??
#server.servlet.session.cookie.secure=true
#FRONT_END_URI=http://localhost:5173
#FRONT_END_URI=http://localhost:5173
#AES SECRET KEY
aes.secret.key=${AES_KEY}