2024.11.05

[Java] RSA 공개키 암호화/복호화 예시( RSA 비대칭키 암호화 )

RSA

  • 공개키 암호화 방식 중 하나로 전자서명이 가능한 최초의 알고리즘 RSA

    • RSA 공개키 방식AES 암호화 방식과 함께 실무에서 가장 많이 사용되고 있는 대표적인 양방향 데이터 암호화 기법 이다.
    • 양방향 데이터 암호화 기법은 데이터 암호화 이후 원본 데이터로 복호화가 가능 하다는 뜻이다.

RSA 비대칭키 (공개키) 암호화 방식

  • RSA 공개키 암호화 방식은 메세지를 암호화할 때 사용되는 공개키(Public Key), 암호화된 메세지를 복호화하기 위한 개인키(Private Key)가 존재하며, 두 개의 키는 한쌍으로 생성되어 관리된다.

  • 이러한 방식은 데이터 암호화/복호화에 사용되는 키가 서로 다르므로 ‘비대칭키 암호화 알고리즘‘이라고도 많이 부른다.

  • RSA 공개키 암호화 방식은 AES 대칭키 암호화 방식에 비해 속도가 느린 단점을 가지고 있으나 서비스 특성에 따라 성능보다 보안에 강점을 갖는 RSA 공개키 암호화 방식을 사용하게 된다.

  • 예를 들면 웹 서비스 접근을 위해 사용되는 SSL/TLS handshake 과정에서 비밀키 (Scret Key) 교환을 위해 RSA 공개키 암호화 방식이 사용된다.

Java로 RSA 공개키 암호화/복호화 구현하기

RSA 공개키 암호화/복호화 Util 생성

package com.work.util;

import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

public class RsaUtil {
	
  private static final String INSTANCE_TYPE = "RSA";

  // 2048bit RSA KeyPair 생성.
  public static KeyPair generateKeypair() throws NoSuchAlgorithmException {
		
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(INSTANCE_TYPE);
    keyPairGen.initialize(2048, new SecureRandom());
		
    return keyPairGen.genKeyPair();
  }
		
  public static String rsaEncode(String plainText, String publicKey)
          throws InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
		
    Cipher cipher = Cipher.getInstance(INSTANCE_TYPE);
    cipher.init(Cipher.ENCRYPT_MODE, convertPublicKey(publicKey));
		
    byte[] plainTextByte = cipher.doFinal(plainText.getBytes());
		
    return base64EncodeToString(plainTextByte);
  }
		
  public static String rsaDecode(String encryptedPlainText, String privateKey)
          throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException {
		
    byte[] encryptedPlainTextByte = Base64.getDecoder().decode(encryptedPlainText.getBytes());
		
    Cipher cipher = Cipher.getInstance(INSTANCE_TYPE);
    cipher.init(Cipher.DECRYPT_MODE, convertPrivateKey(privateKey));
				
    return new String(cipher.doFinal(encryptedPlainTextByte));
  }
		
  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);
  }
}

RSA 2048bit로 KeyPair를 생성 1024bit에 비해 생성 속도는 조금 느리지만 보안을 생각하면 2048bit로 선택하는게 좋다.

convertPublicKey, convertPrivateKey 기능은?

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));
  }

비대칭키 PublicKey와 PrivateKey 생성 후 Client에게 공개키를 전달하거나 Server가 개인키를 보관하기 위한 목적으로 보통 String 변환하여 관리한다.

  • 물론, File로 만들어 관리하기도 하지만 편의성을 생각하면 String 문자열 형태가 용이하다.

  • 한가지 예로 **모바일 웹 서비스에서 E2E 암호화로 RSA 공개키 암호화 방식을 사용한다면?
    1. 사용자 서비스 접근 시 서버에게 Public Key 발급을 위한 REST API 요청
    2. 서버는 Key 생성 후 Public Key 사용자에게 응답 처리, Private Key 서버 Session에 보관
  • 이후 프로세스는 Client 암호화, Server 복호화 과정이 반복된다.

RSA 암호화/복호화 결과 확인

package com.wor.util;

import java.security.KeyPair;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.hyo.test.util.AesUtil;
import com.hyo.test.util.RsaUtil;

class RsaUtilTest {

  private final Logger logger = LoggerFactory.getLogger(this.getClass());
		
  private static String publicKey;
  private static String privateKey;
	
  @Test
  @DisplayName("2048bit RSA KeyPair 생성")
  void generateKeypair() throws Exception {
		
    KeyPair keyPair = RsaUtil.generateKeypair();
    	
    publicKey = RsaUtil.base64EncodeToString(keyPair.getPublic().getEncoded());
    privateKey = RsaUtil.base64EncodeToString(keyPair.getPrivate().getEncoded());
  }
    
  @Test
  @DisplayName("RSA-2048 공개키(비대칭키) 암호화/복호화")
  void testRsa2048() throws Exception {
		
    String plainText = "암호화 테스트";

    String encryptedPlainText = RsaUtil.rsaEncode(plainText, publicKey);
    logger.info("# rsaEncode : {}", encryptedPlainText);
		
    String decryptedPlainText = RsaUtil.rsaDecode(encryptedPlainText, privateKey);
    logger.info("# rsaDecode : {}", decryptedPlainText);
  }
    
  @Test
  @DisplayName("AES-256 대칭키 암호화/복호화")
  void testAse256() throws Exception {
		
    String plainText = "암호화 테스트";

    String encryptedPlainText = AesUtil.aesCBCEncode(plainText);
    logger.info("# aseEncode : {}", encryptedPlainText);
		
    String decryptedPlainText = AesUtil.aesCBCDecode(encryptedPlainText);
    logger.info("# aseDecode : {}", decryptedPlainText);
  }
}

rsaEncode : R2ZpoGI1ATzi9KEgjZmD5a2Vi1a2EZZh3504jRqiWe5zLDXNVzRCQkkHMlhdihe8BfmoE

         wTmjIZNgm6LsMj+CK/kMCZyJ14OpdPo+ZpRPR6ZevB+gKszOaSfH39rWVdAL+g7SJ6b8j
         ynA0+3sbdvx6Gb6q8tiQEBkseXvx20heEIeiPzMOdsSXOr21e7Mtvpif9ESDdFoe3VCVc
         nPI3xUwhFA0jtx4zuummnpOW/ZLFE94Yl5D1C5rJcnbTjWgvY/ELJAKmuE82/KDfz+aTg
         4EKwgsqwmmJsFaIx2K02jdoUwnhxW+0dQznlb1lUDk3Q+2uP6zm6S+kJ3C/uCT/91Q== # rsaDecode : 암호화 테스트 # aseEncode : 66efb73ba12b7434a696dae62d1d10b26ca92cd2173f6689da43c593ae05cc24 # aseDecode : 암호화 테스트 ```