본문 바로가기
IT 개발 이야기/Java

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

by 개발자 Aiden 2023. 2. 23.
반응형

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


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

Java-RSA-공개키-암호화

 

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


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

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

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

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


 

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


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

package com.hyo.test.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. 사용자 서비스 접근 시 서버에게 PublicKey 발급을 위한 REST API 요청
2. 서버는 Key 생성 후 PublicKey 사용자에게 응답 처리, PrivateKey 서버 Session에 보관

이후 프로세스는 계속 설명했던 Client 암호화, Server 복호화 과정이 반복된다.

 

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

package com.hyo.test;

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 = "개발자 Aiden 의 하루";

    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 = "개발자 Aiden 의 하루";

    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 : 개발자 Aiden 의 하루
# aseEncode : 66efb73ba12b7434a696dae62d1d10b26ca92cd2173f6689da43c593ae05cc24
# aseDecode : 개발자 Aiden 의 하루

RSA-JUnit


위 Runner 수행 속도를 보면 RSA 암호화 방식이 KeyPair 생성 과정 포함 AES 암호화 방식보다 성능이 떨어짐을 확인할 수 있다. 실무 경험을 보면 서버 CPU, Disk IO 부하로 성능 이슈는 위에 표기된 단순 수치로 표현할 수 없다.

 

이처럼 RSA 공개키 암호화 방식은 성능보다 보안에 집중되는 알고리즘이다.

- 공개키로 암호화된 데이터는 개인키를 소유한 공개키 생성자만이 복호화 가능하다.
- 사용자( Client )마다 공개키와 개인키를 접근 시 새로 생성/발급하여 원본 데이터 추적이 불가하다.

암호화 방식은 서비스 특성에 따라 적절하게 사용하자.

 

※ AES-256 암호화/복호화 알아보기.

 

[Java] AES 암호화/복호화 예제( AES-256 )

Java AES-256 양방향 데이터 암호화에 대해 알아보자. 정보보안 이슈 대응으로 개인정보( 주민번호 등 ) 데이터에 대한 암호화 정책은 실무에서 필수 적용 항목이다. AES-256 은 현재 실무에서 가장 많

aday7.tistory.com

반응형

댓글


loading