일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Java
- mco
- 자바
- 무디스
- StringBuffer
- 기업분석
- 주린이
- 다형성
- 프로그래머스
- 미국주식
- 현금흐름표
- 배당성장
- 그리디 알고리즘
- 오버라이딩
- etf
- 알고리즘
- 잉여현금흐름
- javascript
- 제태크
- object
- 접근제어자
- 백준
- 인플레이션
- 주식
- 금리인상
- XLF
- 객체지향
- 금리인하
- S&P500
- FCF
- Today
- Total
오늘의하루
[JAVA] RSA 알고리즘에 대해 쉽게 알아보고 구현해보기 본문
RSA 알고리즘은 공개키와 비밀키라는 두 개의 키를 사용하여 데이터를 암호화하고 복호화하는 방식입니다.
이 알고리즘의 핵심은 두 개의 소수를 사용하여 공개키와 비밀키를 생성하고 이를 통해 안전하게 정보를 전달하는 것입니다.
- 공개키 : 공개키는 공유되며 암호화에 사용됩니다.
- 비밀키 : 공유되면 안되며 암호화된 데이터를 복호화하는 데 사용됩니다.
RSA 알고리즘
RSA 알고리즘을 구현하기 위한 몇가지 과정은 아래와 같습니다.
1. 2개의 소수 선택 및 N 추출
소수 P와 Q를 선택한 후 N을 계산합니다.
예를 들어 P = 5, Q = 11를 선택한다면 N = P * Q = 55가 됩니다.
중요한 점은 P와 Q가 노출되면 공개키와 비밀키를 만들 수 있기 때문에 꼭 제거해야합니다.
2. 오일러 피 함수( φ(N) ) 계산
오일러 피 함수( φ(N))는 N과 서로소인 숫자의 개수를 구하는 함수입니다.
φ(N) = (p - 1) * (q - 1)
위 공식에 대입하면 φ(N) = (5 - 1) * (11 - 1) = 4 * 10 = 40이 됩니다.
3. 공개키 E
공개키 E는 1보다 크며 φ(N)보다 작아야합니다.
그리고 E는 φ(N)과 서로소여야 합니다.
1 < E < φ(N)
여기서 1 ~ φ(N) 중에 φ(N)를 소인수 분해한 결과의 배수를 모두 지운 후 일반적으로 가장 작은 수를 선택하게 됩니다.
해당 예시에서는 3이 됩니다.
4. 비밀키 D
비밀키 D는 ED = (K * φ(N)) + 1에 부합하는 수를 의미합니다.
ED = 1(mod φ(N))
E와 D의 곱을 φ(N)으로 나누었을 때 나머지가 1이 되어야 합니다.
이를 풀어서 만들면 ED = (K * φ(N)) + 1 이렇게 만들 수 있습니다.
여기서 K는 배수를 의미하며 현재 예시의 결과로 보면 3d = 41, 81, 121이 되며 d는 27입니다.
5. 암호화
현재 공개키는 N, E가 되며 예를 들어 2를 암호화 하나면 2^E를 N으로 나눈 나머지 즉 8이 됩니다.
6. 복호화
현재 비밀키는 N,D가 되며 암호화된 8을 복호화 한다면 8^D를 N으로 나눈 나머지 즉 2로 복호화됩니다.
Java 직접 RSA 구현
@Slf4j
public class RsaTest {
@Test
public void rsa() {
// 공개되면 큰일나는 소수 두개
BigInteger p = BigInteger.valueOf(1000000007);
BigInteger q = BigInteger.valueOf(1000000009);
// n = p * q ( 공개 )
// n이 충분히 커야 한글과 같이 유니코드 값이 큰 문자도 암호화/복호화 가능
BigInteger n = p.multiply(q);
// φ(N) 계산: φ(N) = (p-1) * (q-1)
BigInteger eulerN = (p.subtract(BigInteger.ONE)).multiply(q.subtract(BigInteger.ONE));
// 공개키 (public key)
BigInteger e = chooseE(eulerN);
// 비밀키 (private key)
BigInteger d = calculateD(e, eulerN);
log.info("Public Key = ({}, {})", n, e);
log.info("private Key = ({}, {})", n, d);
String message = "Hello! RSA!\n안녕 알에스에이\n123456";
log.info("메시지 : {}", message);
// 암호화
StringBuilder encryptedMessage = new StringBuilder();
for (char c : message.toCharArray()) {
// 문자 c를 유니코드 값(BigInteger로 변환)
BigInteger m = BigInteger.valueOf(c);
// 암호화
BigInteger encryptedChar = encryptionOrDecryption(m, e, n);
// 16진수로 변환 후 추가
encryptedMessage.append(encryptedChar.toString(16)).append(" ");
}
log.info("[Hex] 암호화 된 메시지 : {}", encryptedMessage.toString());
// 복호화
String[] encryptedMessageArray = encryptedMessage.toString().split(" ");
StringBuilder decryptedMessage = new StringBuilder();
for (String em : encryptedMessageArray) {
// 16진수 문자열을 BigInteger로 변환
BigInteger encryptedChar = new BigInteger(em, 16);
// 복호화
BigInteger decryptedChar = encryptionOrDecryption(encryptedChar, d, n);
// 복호화된 숫자를 문자로 변환하여 메시지에 추가
decryptedMessage.append((char) decryptedChar.intValue());
}
log.info("복호화 된 메시지 : {}", decryptedMessage.toString());
}
private static BigInteger encryptionOrDecryption(BigInteger m, BigInteger key, BigInteger n) {
return m.modPow(key, n);
}
private static BigInteger calculateD(BigInteger e, BigInteger phiN) {
return e.modInverse(phiN);
}
private BigInteger chooseE(BigInteger phiN) {
BigInteger e = BigInteger.valueOf(2);
while (e.compareTo(phiN) < 0) {
if (e.gcd(phiN).equals(BigInteger.ONE)) {
return e;
}
e = e.add(BigInteger.ONE);
}
return null;
}
}
1. 소수 생성
예제에서는 소수를 직접 입력했지만 실제로 소수를 만들때는 BigInteger.probablePrime(bitLength, random) 메서드를 사용하여 생성할 수 있으며 해당 메소드는 내부적으로 밀러-라빈 알고리즘을 통해 소수 검증을 하며 주어진 비트 길이에 맞는 소수를 반환합니다.
// 1024 : 비트 길이
// new SecureRandom() : 난수 생성기
BigInteger test = BigInteger.probablePrime(1024, new SecureRandom());
2. 공개키와 비밀키의 일치 문제
간혹 공개키와 비밀키가 서로 같아지는 문제가 발생하는데 이를 해결하기 위해서 실제 사용시 e와 d가 같다면 p와 q를 다시 생성하도록 만들어줘야 합니다.
Java 라이브러리로 RSA 구현
@Test
void rsaTest() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
KeyPair keyPair = keyPairGen.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String message = "안녕하세요!\nHello!\nThis is RSA";
System.out.println("origin message = " + message);
// 암호화
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey); // 공개키로 암호화
byte[] encryptedMessage = encryptCipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
System.out.println("encrypted message = " + Base64.getEncoder().encodeToString(encryptedMessage));
// 복호화
Cipher decryptCipher = Cipher.getInstance("RSA");
decryptCipher.init(Cipher.DECRYPT_MODE, privateKey); // 비밀키로 복호화
byte[] decryptedMessage = decryptCipher.doFinal(encryptedMessage);
String decryptedText = new String(decryptedMessage, StandardCharsets.UTF_8);
System.out.println("decrypted message = " + decryptedText);
assertThat(decryptedText).isEqualTo(message);
}
결론
RSA는 매우 큰 수의 연산을 기반으로 하기 때문에 짧은 데이터를 암호화하고 복호화하는 데 적합합니다.
긴 데이터를 처리하려면 효율성이 떨어질 수 있으므로 일반적으로 RSA는 대칭 키 암호화를 위한 키 교환이나 디지털 서명 같은 용도로 사용된다고 합니다.
긴 데이터의 경우 다른 암호화 알고리즘을 사용하는 것이 적합합니다.
'Algorithm' 카테고리의 다른 글
[Greedy Algorithm] 백준 2217번 : 로프(Java) (0) | 2023.08.04 |
---|---|
[Greedy Algorithm] 백준 5585번 : 거스름돈 (Java) (0) | 2023.08.04 |
[Greedy Algorithm] 백준 1541번 : 잃어버린 괄호 (Java) (0) | 2023.08.04 |
[Greedy Algorithm] 백준 1931번 : 회의실 배정 (Java) (0) | 2023.08.04 |
[Greedy Algorithm] 백준 11047번 : 동전 0(Java) (0) | 2023.08.04 |