Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- rsa java
- aws lightsail 배포
- Docker
- 내도메인 한국
- rsa 복호화
- Stored Procedure log
- aws lightsail
- 지속쿠키
- 플러터
- RSA
- Flutter
- jenkins git ignore file 추가
- fk컬럼 삭제
- 세션쿠키
- git
- lightsail 도메인 연결
- docker error
- springboot 배포
- 하이브리드암호화
- aws
- jstl dependency
- 제약조건 제거
- jstl 종류
- 하이브리드 암호화
- springboot3 jstl
- AES
- XSS 예방
- 스왑 메모리
- spring
- swap file
Archives
- Today
- Total
easycode
하이브리드 암호화 (RSA, AES) (2) - 구현 본문
2024.02.01 - [분류 전체보기] - 하이브리드 암호화 (RSA, AES) (1) - 개념 설명
이전 글에 이어서 이번엔 코드로 구현입니다! 제 개발환경은 SpringBoot 2.x(gradle) 버전, JDK 11을 사용하고 있습니다.
누군가에게 이 글이 도움이 되기를 바라면서 시작하겠습니다.
전체적인 코드는 아래를 봐주세요! 그러나 중간에 프론트엔드와 소통하는 중에 발생한 오류 수정으로 인해 최종본은 아래에 있는 github 코드로 확인해주세요!!
https://github.com/wooyeon0626/wooyeon/pull/62
Github 코드 (최종본)
[UserService] RSA 복호화 및 AES Key + iv로 복호화 https://github.com/wooyeon0626/wooyeon/blob/feature/join/src/main/java/com/wooyeon/yeon/user/service/UserService.java
암호화 진행순서
암호화 진행순서는 다음과 같습니다.
- 백엔드에서 RSA key pair 생성
- 프론트엔드로 RSA public key(공개키) 전송
- 프론트엔드에서 사용자에게 비밀번호를 받아서 AES128/CBC 방식으로 암호화 (encryptedPassword)
- AES key와 iv(CBC 방식에 필요)를 합쳐 백엔드에게 받은 RSA public key로 암호화 (encryptedKey) -> 여기서 aes key를 세션키라고도 부른다.
- 백엔드에게 암호화된 비밀번호와 암호화된 key 전달
- 백엔드는 암호화된 키(encryptedKey)를 RSA secret key(개인키)로 복호화
- 복호화된 평문(key)에서 AES key 추출
- 암호화된 비밀번호를 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
→ 암호화된 구문을 복호화할 때 발생할 수 있는 오류로, 암호화 때 사용한 비밀키와 복호화 할 때의 비밀키가 일치하지 않았을 때 발생한다.