Java RSA 비대칭 암호화 구현 개인키, 공개키 생성
RSA 란
RSA 는 간단히, 공개키로 암호화를 하고 개인키로 복호화를 하는 암호화 알고리즘 입니다.
SSL에서 가장 많이 사용되고 대부분의 인터넷 뱅킹에서 RSA-2048을 사용한다고 합니다.
자세한 내용이 궁금하신 분들은 구글링 하면 정보가 많이 나오니 참고하시기 바랍니다.
특징
위에서 얘기한 것처럼 서버에서는 공개키(Public Key)와 개인키(Private Key) 쌍을 생성하여 요청하는 Client에게 공개키를 전달 합니다. Client 는 받은 공개키를 사용해 개인정보를 암호화 한 후 server로 전달합니다.
Server는 공개키로 암호화된 데이터를 개인키를 사용하여 복호화 합니다.
구현
이제 구현을 해보겠습니다.
키는 서버 어플리케이션이 실행될 때 특정 폴더에 키가 있으면 기존키를 사용하고, 없으면 새로 생성하도록 하겠습니다. 파일로 관리하는 이유는 이중화 시 서버가 같은 키를 공유하게 하기위해서 입니다.
[RsaKeyGenerator.class]
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.Key;
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.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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RsaKeyGenerator implements InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(RsaKeyGenerator.class);
private final String path;
private final String algorithm;
private final int keySize;
public RsaKeyGenerator(@Value("${keyPair.path}") String path,
@Value("${keyPair.algorithm}") String algorithm, @Value("${keyPair.keySize}") int keySize) {
this.path = path;
this.algorithm = algorithm;
this.keySize = keySize;
}
@Override
public void afterPropertiesSet() throws NoSuchAlgorithmException, IOException {
if (!keyFileCheck()) {
createKeyFile();
}else{
LOGGER.info("RSA 키가 존재하여 기존 키를 활용합니다.");
}
}
/**
* 키 파일이나 폴더가 존재하는지 체크하는 메소드
*/
private boolean keyFileCheck() {
File folder = new File(this.path);
if (!folder.exists()) {
return false;
} else {
String[] files = new String[] { this.path + "public.pem", this.path + "private.pem" };
for (String f : files) {
File file = new File(f);
if (!file.exists())
return false;
}
}
return true;
}
/**
* 키 파일을 생성하는 메소드, 무조건 파일을 모두 새로 생성한다.
*/
private void createKeyFile() throws IOException, NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(this.algorithm);
keyPairGenerator.initialize(this.keySize);
KeyPair keyPair = keyPairGenerator.genKeyPair();
Key[] keys = new Key[] { keyPair.getPublic(), keyPair.getPrivate() };
FileOutputStream fos = null;
try {
File folder = new File(this.path);
if (!folder.exists()){
folder.mkdir();
}
File[] files = folder.listFiles();
for (File f : files) {
f.delete();
}
for (Key key : keys) {
String path = null;
if (key.equals(keyPair.getPublic())) {
path = this.path + "public.pem";
} else {
path = this.path + "private.pem";
}
File file = new File(path);
fos = new FileOutputStream(file);
fos.write(key.getEncoded());
LOGGER.info("RSA 키를 새로 생성하였습니다.");
}
} catch (IOException e) {
throw e;
} finally {
if (fos != null) {
fos.close();
fos.flush();
}
}
}
/**
* 키 파일을 읽어 리턴하는 메소드, 없을 경우 새로 생성한다.
*/
public PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
if (!keyFileCheck()) {
createKeyFile();
}
byte[] bytes = Files.readAllBytes(Paths.get(this.path + "private.pem"));
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance(this.algorithm);
PrivateKey pk = keyFactory.generatePrivate(spec);
return pk;
}
/**
* 키 파일을 읽어 리턴하는 메소드, 없을 경우 새로 생성한다.
*/
public PublicKey getPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
if (!keyFileCheck()) {
createKeyFile();
}
byte[] bytes = Files.readAllBytes(Paths.get(this.path + "public.pem"));
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance(this.algorithm);
PublicKey pk = keyFactory.generatePublic(spec);
return pk;
}
/**
* public 키로 암호화를 한다.
*/
public String encryptRSA(String plainText) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException{
PublicKey publicKey = getPublicKey();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bytePlain = cipher.doFinal(plainText.getBytes());
String encrypted = Base64.getEncoder().encodeToString(bytePlain);
return encrypted;
}
/**
* private 키로 복호화를 한다.
*/
public String decryptRSA(String encrypted) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException{
PrivateKey privateKey = getPrivateKey();
Cipher cipher2 = Cipher.getInstance("RSA");
byte[] byteEncrypted = Base64.getDecoder().decode(encrypted.getBytes());
cipher2.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bytePlain = cipher2.doFinal(byteEncrypted);
String decrypted = new String(bytePlain, "utf-8");
return decrypted;
}
}
RsaKeyGenerator는 전체적인 RSA기능을 담당합니다.
InitializingBean을 implements 받아 클래스 변수를 Properties 설정값으로 초기화 합니다.
path는 키쌍을 저장할 서버 경로
algorithm은 암호화 알고리즘 (RSA)
keySize 는 암호화 키 사이즈 (보통 2048) 을 설정합니다.
afterPropertiesSet() 메소드는 말 그대로 properties 값이 setting 된 후 실행되는 메소드로,
키 파일이 있는지 체크를 하고, 없는 경우에 새로 생성하는 역할을 합니다.
createKeyFile() 메소드는 키를 생성하는 역할을 합니다.
KeyPairGenerator 에 알고리즘과 사이즈를 설정하고 키 쌍(KeyPair) 를 생성합니다.
생성한 KeyPair로 부터 public, private 키를 얻을 수 있습니다.
아래 로직은 public.pem, private.pem 파일에 생성한 키를 인코딩하여 저장하는 코드 입니다.
키는 쌍이기 때문에 생성해야되는 경우 항상 새로 생성합니다.
getPrivateKey(), getPublicKey() 메소드는 파일의 데이터를 읽어 각각의 키 인터페이스를 리턴합니다.
encryptRSA(), decryptRSA() 메소드는 publicKey로 암호화 private키로 복호화를 하는 메소드 입니다.
이제 테스트를 위한 Controller를 구현합니다.
@RestController
@RequiredArgsConstructor
public class RsaController {
private final RsaKeyGenerator keyGen;
@PostMapping("/publicKey")
@ApiOperation(value = "RSA publicKey 요청", notes = "RSA public key를 요청할 때 호출한다.")
@Description("JWT Token 인증을 받은 후 요청 가능하다. Authorization: Bearer Token 값으로 Token 을 담아보낸다.")
@CustomApiResponse
public ResponseEntity<PublicKeyVO> getPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
PublicKey key = keyGen.getPublicKey();
String strKey = Base64.getEncoder().encodeToString(key.getEncoded());
PublicKeyVO publicKey = new PublicKeyVO(strKey);
return new ResponseEntity<>(publicKey, HttpStatus.OK);
}
}
swagger 관련 어노테이션은 생략하셔도 됩니다.
swagger나 requiredArgsConstructor 에 대해서는 아래의 Link를 참고하세요.
Link : https://aljjabaegi.tistory.com/668
Link : https://aljjabaegi.tistory.com/646
publicKeyVO 는 단순히 키값만 있는 Value Object Class 입니다.
이제 해당 요청을 해보도록 하죠.
Postman을 사용하여 요청을 하면 위와같은 결과가 나오게 됩니다.
Client는 해당 PublicKey를 이용하여 데이터를 암호화 해서 전송하고 서버는 PrivateKey를 이용하여 복호화하게 됩니다.
아래 사이트에 가시면 생성된 private 에 해당하는 public 키를 확인 하실 수 있습니다.
Link : https://travistidwell.com/jsencrypt/demo/