Skip to content

Commit

Permalink
Merge pull request #51 from CSID-DGU/feature/#48/upbit-keys
Browse files Browse the repository at this point in the history
[feat] : 유저 업비트 키 등록 API 구현 (POST)
  • Loading branch information
bbbang105 authored Jun 7, 2024
2 parents 99c5f8b + 38ddaa5 commit 6c50682
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public enum SuccessStatus implements BaseCode {
// DashBoard
SUCCESS_GET_USER_BALANCE(HttpStatus.OK, "200", "유저 업비트 잔고 조회에 성공했습니다"),
SUCCESS_GET_USER_COINS(HttpStatus.OK, "200", "유저 보유 코인 조회에 성공했습니다"),
SUCCESS_GET_REPRESENTATIVE_COINS(HttpStatus.OK, "200", "대표 코인 조회에 성공했습니다");
SUCCESS_GET_REPRESENTATIVE_COINS(HttpStatus.OK, "200", "대표 코인 조회에 성공했습니다"),
// Upbit-Key
SUCCESS_ADD_UPBIT_KEYS(HttpStatus.CREATED, "201", "업비트 키 등록에 성공했습니다");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.dgu.backend.controller;

import lombok.RequiredArgsConstructor;
import org.dgu.backend.common.ApiResponse;
import org.dgu.backend.common.constant.SuccessStatus;
import org.dgu.backend.dto.UserDto;
import org.dgu.backend.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;

// 유저 업비트 키 등록 API
@PostMapping("/upbit-keys")
public ResponseEntity<ApiResponse<Object>> addUserUpbitKeys(
@RequestHeader("Authorization") String authorizationHeader,
@RequestBody UserDto.UserUpbitKeyRequest userUpbitKeyRequest) {

userService.addUserUpbitKeys(authorizationHeader, userUpbitKeyRequest);
return ApiResponse.onSuccess(SuccessStatus.SUCCESS_ADD_UPBIT_KEYS);
}
}
31 changes: 28 additions & 3 deletions backend/src/main/java/org/dgu/backend/domain/UpbitKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -22,11 +23,35 @@ public class UpbitKey {
private User user;

@Column(name = "upbit_keys_uuid", columnDefinition = "BINARY(16)", unique = true)
private UUID upbitKey;
private UUID upbitKeyId;

@Column(name = "access_key", nullable = false, length = 100)
@Column(name = "access_key", nullable = false, columnDefinition = "LONGTEXT")
private String accessKey;

@Column(name = "secret_key", nullable = false, length = 100)
@Column(name = "secret_key", nullable = false, columnDefinition = "LONGTEXT")
private String secretKey;

@Column(name = "private_key", nullable = false, columnDefinition = "LONGTEXT")
private String privateKey;

@Builder
public UpbitKey(User user, String accessKey, String secretKey, String privateKey) {
this.user = user;
this.upbitKeyId = UUID.randomUUID();
this.accessKey = accessKey;
this.secretKey = secretKey;
this.privateKey = privateKey;
}

public void updateAccessKey(String accessKey) {
this.accessKey = accessKey;
}

public void updateSecretKey(String secretKey) {
this.secretKey = secretKey;
}

public void updatePrivateKey(String privateKey) {
this.privateKey = privateKey;
}
}
20 changes: 20 additions & 0 deletions backend/src/main/java/org/dgu/backend/dto/UserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.dgu.backend.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

public class UserDto {
@Builder
@Getter
@AllArgsConstructor
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class UserUpbitKeyRequest {
private String accessKey;
private String secretKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.dgu.backend.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.dgu.backend.common.code.BaseErrorCode;
import org.dgu.backend.common.dto.ErrorReasonDto;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum EncryptionErrorResult implements BaseErrorCode {
KEY_PAIR_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "키 쌍 생성에 실패했습니다."),
SECRET_KEY_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "시크릿 키생성에 실패했습니다."),
RSA_ENCRYPTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "RSA 암호화에 실패했습니다."),
RSA_DECRYPTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "RSA 복호화에 실패했습니다."),
AES_ENCRYPTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "AES 암호화에 실패했습니다."),
AES_DECRYPTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "500", "AES 복호화에 실패했습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ErrorReasonDto getReason() {
return ErrorReasonDto.builder()
.isSuccess(false)
.code(code)
.message(message)
.build();
}

@Override
public ErrorReasonDto getReasonHttpStatus() {
return ErrorReasonDto.builder()
.isSuccess(false)
.httpStatus(httpStatus)
.code(code)
.message(message)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.dgu.backend.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class EncryptionException extends RuntimeException {
private final EncryptionErrorResult encryptionErrorResult;

@Override
public String getMessage() {
return encryptionErrorResult.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ public ResponseEntity<ApiResponse<BaseErrorCode>> handleUpbitException(UpbitExce
UpbitErrorResult errorResult = e.getUpbitErrorResult();
return ApiResponse.onFailure(errorResult);
}
// Encryption
@ExceptionHandler(EncryptionException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleEncryptionException(EncryptionException e) {
EncryptionErrorResult errorResult = e.getEncryptionErrorResult();
return ApiResponse.onFailure(errorResult);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.dgu.backend.service;

import org.dgu.backend.dto.UserDto;

public interface UserService {
void addUserUpbitKeys(String authorizationHeader, UserDto.UserUpbitKeyRequest userUpbitKeyRequest);
}
69 changes: 69 additions & 0 deletions backend/src/main/java/org/dgu/backend/service/UserServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.dgu.backend.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.dgu.backend.domain.UpbitKey;
import org.dgu.backend.domain.User;
import org.dgu.backend.dto.UserDto;
import org.dgu.backend.repository.UpbitKeyRepository;
import org.dgu.backend.util.AESUtil;
import org.dgu.backend.util.EncryptionUtil;
import org.dgu.backend.util.JwtUtil;
import org.springframework.stereotype.Service;

import java.security.KeyPair;
import java.util.Base64;

@Service
@Transactional
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UpbitKeyRepository upbitKeyRepository;
private final JwtUtil jwtUtil;
private final EncryptionUtil encryptionUtil;
private final AESUtil aesUtil;

// 유저 업비트 키를 등록하는 메서드
@Override
public void addUserUpbitKeys(String authorizationHeader, UserDto.UserUpbitKeyRequest userUpbitKeyRequest) {
User user = jwtUtil.getUserFromHeader(authorizationHeader);
UpbitKey existUpbitKey = upbitKeyRepository.findByUser(user);

KeyPair keyPair = encryptionUtil.generateKeyPair();
String encodedAccessKey = encryptAndEncode(userUpbitKeyRequest.getAccessKey(), keyPair);
String encodedSecretKey = encryptAndEncode(userUpbitKeyRequest.getSecretKey(), keyPair);
String encryptedPrivateKey = encryptPrivateKey(keyPair);

saveUpbitKey(user, encodedAccessKey, encodedSecretKey, encryptedPrivateKey, existUpbitKey);
}

// 암호화 및 인코딩 메서드
private String encryptAndEncode(String data, KeyPair keyPair) {
byte[] encryptedData = encryptionUtil.encrypt(data.getBytes(), keyPair.getPublic());
return Base64.getEncoder().encodeToString(encryptedData);
}

// 프라이빗 키 암호화 메서드
private String encryptPrivateKey(KeyPair keyPair) {
return aesUtil.encrypt(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
}

// 업비트 키 저장 메서드
private void saveUpbitKey(User user, String encodedAccessKey, String encodedSecretKey, String encryptedPrivateKey, UpbitKey existUpbitKey) {
// 기존 키 있는 경우 업데이트
if (existUpbitKey != null) {
existUpbitKey.updateAccessKey(encodedAccessKey);
existUpbitKey.updateSecretKey(encodedSecretKey);
existUpbitKey.updatePrivateKey(encryptedPrivateKey);
upbitKeyRepository.save(existUpbitKey);
} else {
UpbitKey upbitKey = UpbitKey.builder()
.user(user)
.accessKey(encodedAccessKey)
.secretKey(encodedSecretKey)
.privateKey(encryptedPrivateKey)
.build();
upbitKeyRepository.save(upbitKey);
}
}
}
63 changes: 63 additions & 0 deletions backend/src/main/java/org/dgu/backend/util/AESUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.dgu.backend.util;

import lombok.RequiredArgsConstructor;
import org.dgu.backend.exception.EncryptionErrorResult;
import org.dgu.backend.exception.EncryptionException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

@Component
@RequiredArgsConstructor
public class AESUtil {

@Value("${aes.secret}")
private String SECRET_KEY;

private SecretKey generateKey() {
try {
// 시크릿 키를 SHA-256 해시로 변환하여 32바이트 길이로 만듦
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = sha.digest(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
return new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new EncryptionException(EncryptionErrorResult.SECRET_KEY_GENERATION_FAILED);
}
}

// AES 암호화 메서드
public String encrypt(String data) {
try {
SecretKey secretKey = generateKey();

Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
throw new EncryptionException(EncryptionErrorResult.AES_ENCRYPTION_FAILED);
}
}

// AES 복호화 메서드
public String decrypt(String encryptedData) {
try {
SecretKey secretKey = generateKey();

Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));

return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException(EncryptionErrorResult.AES_DECRYPTION_FAILED);
}
}
}
64 changes: 64 additions & 0 deletions backend/src/main/java/org/dgu/backend/util/EncryptionUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.dgu.backend.util;

import lombok.RequiredArgsConstructor;
import org.dgu.backend.exception.EncryptionErrorResult;
import org.dgu.backend.exception.EncryptionException;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

@Component
@RequiredArgsConstructor
public class EncryptionUtil {
private static final int KEY_SIZE = 2048;
private final AESUtil aesUtil;

// RSA 암호화에 사용할 키 쌍 생성
public KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException(EncryptionErrorResult.KEY_PAIR_GENERATION_FAILED);
}
}

// RSA 암호화 메서드
public byte[] encrypt(byte[] input, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(input);
} catch (Exception e) {
throw new EncryptionException(EncryptionErrorResult.RSA_ENCRYPTION_FAILED);
}
}

// RSA 복호화 메서드
public byte[] decrypt(byte[] input, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(input);
} catch (Exception e) {
throw new EncryptionException(EncryptionErrorResult.RSA_DECRYPTION_FAILED);
}
}

// 프라이빗 키 복호화 메서드
public PrivateKey getDecryptedPrivateKey(String encryptedPrivateKey) {
try {
String decryptedKey = aesUtil.decrypt(encryptedPrivateKey);
byte[] keyBytes = Base64.getDecoder().decode(decryptedKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt private key", e);
}
}
}
Loading

0 comments on commit 6c50682

Please sign in to comment.