easycode

하이브리드 암호화 (RSA, AES) (2) - 구현 본문

카테고리 없음

하이브리드 암호화 (RSA, AES) (2) - 구현

ez() 2024. 2. 1. 22:10

2024.02.01 - [분류 전체보기] - 하이브리드 암호화 (RSA, AES) (1) - 개념 설명

 

하이브리드 암호화 (RSA, AES) (1) - 개념 설명

프로젝트에서 비밀번호 암호화를 해야 하는데, 프론트엔드(클라이언트)와 데이터를 주고 받는 과정에서 혹시나 중간에 비밀번호가 탈취 당할 가능성을 생각해 이리저리 알아보다가 하이브리드

easyoungcode.tistory.com

이전 글에 이어서 이번엔 코드로 구현입니다! 제 개발환경은 SpringBoot 2.x(gradle) 버전, JDK 11을 사용하고 있습니다.

누군가에게 이 글이 도움이 되기를 바라면서 시작하겠습니다.

 

 

전체적인 코드는 아래를 봐주세요! 그러나 중간에 프론트엔드와 소통하는 중에 발생한 오류 수정으로 인해 최종본은 아래에 있는 github 코드로 확인해주세요!!

https://github.com/wooyeon0626/wooyeon/pull/62

 

[FEAT] AES Util 추가 & 클라이언트에게 RSA로 암호화된 encryptedKey 받아와서 decode & AES KEY와 IV로 나눈

AES Util 추가 클라이언트에게 RSA로 암호화된 encryptedKey 받아와서 RSA 개인키로 복호화 decode하기 1번의 decodeKey를 AES Key와 IV로 분리하기 AES Key와 IV 값으로 encryptedPassword Decode 하기 salt 생성 구현 SHA256

github.com

Github 코드 (최종본)

[RSA Util] https://github.com/wooyeon0626/wooyeon/blob/feature/join/src/main/java/com/wooyeon/yeon/user/service/encrypt/RsaUtil.java 

[AES Util] https://github.com/wooyeon0626/wooyeon/blob/feature/join/src/main/java/com/wooyeon/yeon/user/service/encrypt/AesUtil.java

[UserService] RSA 복호화 및 AES Key + iv로 복호화 https://github.com/wooyeon0626/wooyeon/blob/feature/join/src/main/java/com/wooyeon/yeon/user/service/UserService.java

 


암호화 진행순서

암호화 진행순서는 다음과 같습니다.

  1. 백엔드에서 RSA key pair 생성
  2. 프론트엔드로 RSA public key(공개키) 전송
  3. 프론트엔드에서 사용자에게 비밀번호를 받아서 AES128/CBC 방식으로 암호화 (encryptedPassword)
  4. AES key와 iv(CBC 방식에 필요)를 합쳐 백엔드에게 받은 RSA public key로 암호화 (encryptedKey) -> 여기서 aes key를 세션키라고도 부른다.
  5. 백엔드에게 암호화된 비밀번호와 암호화된 key 전달
  6. 백엔드는 암호화된 키(encryptedKey)를 RSA secret key(개인키)로 복호화
  7. 복호화된 평문(key)에서 AES key 추출
  8. 암호화된 비밀번호를 AES key와 iv를 통해 복호화
    복호화된 평문(password)에 salt 추가 후, SHA256으로 암호화 (salt는 rainbow 테이블 방지하기 위해 사용)

 


RSA key pair 생성

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@Component // Bean으로 등록하기 위해 사용해주었습니다
@Log4j2
public class RsaUtil {
    private static final String INSTANCE_TYPE = "RSA";
    private static final KeyPair keyPair = generateKeyPair();

	// RSA keyPair 생성
    private static KeyPair generateKeyPair() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(INSTANCE_TYPE);
            keyPairGenerator.initialize(2048, new SecureRandom());
            KeyPair keyPair = keyPairGenerator.genKeyPair();

            return keyPair;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Error generating RSA key pair", e);
        }
    }

	// RSA 암호화
    public static String rsaEncode(String password, String publicKey)
            throws InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        Cipher cipher = Cipher.getInstance(INSTANCE_TYPE);
        cipher.init(Cipher.ENCRYPT_MODE, convertPublicKey(publicKey));

        byte[] passwordByte = cipher.doFinal(password.getBytes());
        return base64EncodeToString(passwordByte);
    }

	// RSA 복호화
    public static byte[] rsaDecode(String encryptedPassword, String privateKey)
            throws InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        byte[] encryptedPasswordByte = Base64.getDecoder().decode(encryptedPassword.getBytes());

        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // ** INSTANCE TYPE은 꼭 명확하게 명시해주기!!!
        cipher.init(Cipher.DECRYPT_MODE, convertPrivateKey(privateKey));

        return Base64.getDecoder().decode(cipher.doFinal(encryptedPasswordByte));
    }

    public static PublicKey convertPublicKey(String publicKey)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        KeyFactory keyFactory = KeyFactory.getInstance(INSTANCE_TYPE);
        byte[] publicKeyByte = Base64.getDecoder().decode(publicKey.getBytes());

        return keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyByte));
    }

    public static PrivateKey convertPrivateKey(String privateKey)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        KeyFactory keyFactory = KeyFactory.getInstance(INSTANCE_TYPE);
        byte[] privateKeyByte = Base64.getDecoder().decode(privateKey.getBytes());

        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyByte));
    }

    public static String base64EncodeToString(byte[] byteData) {
        return Base64.getEncoder().encodeToString(byteData);
    }

	// 프론트엔드에게 publicKey를 보내주기 위해 생성, PublicKey get 메서드
    public static String sendPublicKey() {
        return base64EncodeToString(keyPair.getPublic().getEncoded());
    }
    
    // PrivateKey get 메서드
    public static String sendPrivateKey() {
        return base64EncodeToString(keyPair.getPrivate().getEncoded());
    }
}

 


RSA 공개키 전송 API

// RSA 공개키 전송
    @GetMapping("/encrypt/key")
    public RsaPublicResponseDto sendRsaPublicKey() {
        RsaPublicResponseDto rsaPublicResponseDto = userService.sendRsaPublicKey();

        return rsaPublicResponseDto;
    }

 

 

RSA 개인키로 복호화

// RSA 개인키로 Session Key(AES Key) 복호화
byte[] decodedKey = RsaUtil.rsaDecode(base64AesKey, RsaUtil.sendPrivateKey());
log.info("디코딩된 IV: {}", ivBytes);
log.info("복호화된 AES Key: {}", decodedKey);
  • base64AesKey는 base64로 인코딩된 String형태의 AES Key입니다.
  • ivBytes는 byte형태의 iv값입니다. 저희는 AES128/CBC 방식을 사용했기에 iv가 있습니다.

 

 

byte[] 형태의 iv와 aes key를 사용해 비밀번호 복호화(aes 복호화)

// IV, SessionKey로 암호화된 비밀번호 복호화
// AES Key 로 비밀번호 복호화해서 원문 받아오기
String decodedPassword = aesUtil.decrypt(passwordEncryptRequestDto.getEncryptedPassword(), decodedKey, ivBytes);
log.info("AES로 복호화한 원문 : {}", decodedPassword);

 

 

 

 


[번외] 마주쳤던 오류들과 에러 이유

java.security.InvalidKeyException: Invalid key length

 → 복호화 키의 길이가 기준에 맞지 않는 경우 발생한다 (AES Key는 16, 24, 32 byte여야 한다!!)

 

javax.crypto.BadPaddingException: Given final block not properly padded

 → 암호화된 구문을 복호화할 때 발생할 수 있는 오류로, 암호화 때 사용한 비밀키와 복호화 할 때의 비밀키가 일치하지 않았을 때 발생한다.