본문 바로가기
  • Code Smell
Copy&Paste

[Java] RSA 양방향 암호화

by HSooo 2020. 8. 14.

RSA 양방향 암호화

Github

AES/SEED 암호화는 하나의 키를 가지고 암/복호화를 같이하는 대칭키(=암호화/복호화 키가 같음) 암호화 방식이라면
RSA는 암호화 키와 복호화 키가 다른 비대칭키 암호화 이다.

키를 만드는 과정

1. 두 소수 p , q를 준비한다.
2. p - 1, q - 1과 각각 서로소인 정수 e를 준비한다.
3. ed를 (p - 1)(q - 1)으로 나눈 나머지가 1이 되도록 하는 d를 찾는다.
4. N = pq를 계산한 후, N와 e를 공개한다. 이들이 바로 공개키이다. 한편 d는 숨겨두는데, 이 수가 바로 개인키이다.
5. 이제 p, q, (p - 1)(q - 1)는 필요 없거니와 있어 봐야 보안에 오히려 문제를 일으킬 수 있으니, 파기한다.

나무위키 RSA 설명

공캐키가 달라지면 계산법에 의해서 개인키도 같이 달라지므로, 어느 하나만 일방적으로 만들 순 없다.
자바에는 키 생성 및 암복호화를 java.crypto, java.security 패키지에 제공한다.

설명에도 나와있듯이 암호화 하는 키를 개인키(public key)라 하고, 복호화 하는 키를 개인키(private key)라고 하는데, 개인키는 여기저기 공개해도 상관없지만 개인키는 데이터를 볼 대상만이 가지고 있어야 한다.
그래서 데이터를 암호화 하는건 아무나 할 수 있어도 복호화는 특정 대상만 할 수 있게 해서, 누군가 데이터를 중간에 가로채도 실제 데이터를 볼 수 없게 해놨다.

RSA는 반대 방향의 암복호화도 가능한데, 개인키로 암호화, 공개키로 복호화도 가능하다.
이렇게 역방향으로 사용하는 방식이 공인인증서에 사용된다.

소스

추가로 필요한 라이브러리는 없음

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
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;

/**
 * RSA 암호화
 *
 * @author https://sunghs.tistory.com
 * @see <a href="https://github.com/sunghs/java-utils">source</a>
 */
public class Rsa {

    private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8;

    private static final String INSTANCE_TYPE = "RSA";

    private final Class<?> keyClass;

    private final Key keyInstance;

    private Cipher cipher;

    public Rsa(final Key key) {
        if (key instanceof PublicKey) {
            keyClass = PublicKey.class;
            keyInstance = key;
        } else if (key instanceof PrivateKey) {
            keyClass = PrivateKey.class;
            keyInstance = key;
        } else {
            throw new ClassCastException("unknown key class");
        }

        try {
            cipher = Cipher.getInstance(INSTANCE_TYPE);
        } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    /**
     * key 객체 String 형으로 인코딩
     *
     * @param key key
     * @return String encodedKey
     */
    public static String encodeKey(final Key key) {
        return new String(Base64.getEncoder().encode(key.getEncoded()));
    }

    /**
     * String 으로 인코딩 된 private key 를 객체로 변환
     *
     * @param encodedKey encodedKey
     * @return PrivateKey key
     */
    public static PrivateKey decodePrivateKey(final String encodedKey) {
        try {
            byte[] bKey = Base64.getDecoder().decode(encodedKey.getBytes(ENCODING_TYPE));
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(bKey);
            KeyFactory keyFactory = KeyFactory.getInstance(INSTANCE_TYPE);
            return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * String 으로 인코딩 된 public key 를 객체로 변환
     *
     * @param encodedKey encodedKey
     * @return PublicKey key
     */
    public static PublicKey decodePublicKey(final String encodedKey) {
        try {
            byte[] bKey = Base64.getDecoder().decode(encodedKey.getBytes(ENCODING_TYPE));
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(bKey);
            KeyFactory keyFactory = KeyFactory.getInstance(INSTANCE_TYPE);
            return keyFactory.generatePublic(x509EncodedKeySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * PublicKey, PrivateKey 생성
     *
     * @param size 생성될 키의 지정 값 (비트 수), 1024, 2048 권장
     * @return KeyPair
     */
    public static KeyPair generateKey(int size) {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(INSTANCE_TYPE);
            keyPairGenerator.initialize(size);
            return keyPairGenerator.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public String encrypt(final String str) throws InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        if (keyClass.isAssignableFrom(PublicKey.class)) {
            cipher.init(Cipher.ENCRYPT_MODE, keyInstance);
            byte[] encrypted = cipher.doFinal(str.getBytes(ENCODING_TYPE));
            return new String(Base64.getEncoder().encode(encrypted), ENCODING_TYPE);
        } else {
            throw new ClassCastException("not public key set");
        }
    }

    public String decrypt(final String str) throws InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        if (keyClass.isAssignableFrom(PrivateKey.class)) {
            cipher.init(Cipher.DECRYPT_MODE, keyInstance);
            byte[] decrypted = Base64.getDecoder().decode(str.getBytes(ENCODING_TYPE));
            return new String(cipher.doFinal(decrypted), ENCODING_TYPE);
        } else {
            throw new ClassCastException("not private key set");
        }
    }
}

하나의 객체로 암복호화를 하기보다는 한쪽에서는 암호화를 하고 한쪽에서는 복호화를 하므로
객체별로 암호화용, 복호화용으로 쓸 수 있도록 해놓았다. 조금 변경하면 하나의 객체가 암복호화를 할 수 있도록 할 수 있을 것이다.

키 생성 테스트

generateKey의 인자는 1024, 2048을 추천한다. (RSA-1024, RSA-2048)

@Test
public void createKeyPair() {
    KeyPair keyPair = Rsa.generateKey(1024);
    Optional.ofNullable(keyPair).ifPresent(k -> {
        PublicKey publicKey = k.getPublic();
        PrivateKey privateKey = k.getPrivate();

        log.info("public : {}", Rsa.encodeKey(publicKey));
        log.info("private : {}", Rsa.encodeKey(privateKey));
    });
}

결과

INFO sunghs.java.utils.cipher.RsaTest - public : MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCjJvSILNVXQJSjO6Qm98u4MNp9nNPmz1XhGIUC8jGoksMEzTcQgeJGBWL0Q19AzJUdS4tWTTUCVQooEwk9/HIiqsGXBwP1hgsOZlRGddwXqxdvBpAmnOCZOh2pYMCyRtXs5OYOMnw6CaYNmiRSRWsd2/AvjrX8pKadwZiknzjbpwIDAQAB


INFO sunghs.java.utils.cipher.RsaTest - private : MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKMm9Igs1VdAlKM7pCb3y7gw2n2c0+bPVeEYhQLyMaiSwwTNNxCB4kYFYvRDX0DMlR1Li1ZNNQJVCigTCT38ciKqwZcHA/WGCw5mVEZ13BerF28GkCac4Jk6HalgwLJG1ezk5g4yfDoJpg2aJFJFax3b8C+Otfykpp3BmKSfONunAgMBAAECgYBE11oU334Be/F70t2Xx7UA+jQnDnZnDJM7EIKHVLRZYdvB+elDINreGsW/NXJKwEgm/UpE1v0IB+PqNuYObqelhplHjtUibfDUdwH1SAdMpa9LREC0MXk9nhaZ9d6p5Gj9ZUWkk75f7UCfhXJ0Iml7I5ARV1LlB4i9qvUNb7RPmQJBAM016GTBvP9p2XJzUJlRgqOrtP6RIg8jm1U7L4k43Tp+rt/jBDwQA8ygwweUr2GXFQfdlxwifaBGqRoYul5hWL0CQQDLiDyk5mIVHrxm4b6fIWHHdfAxOs7zWtByD5G2fmakPYbIIjXp/X6ECi5wJECOoABzK+zWCZeG9uYTjonUGsYzAkBcrk8ySmnwtT63OSuawzyMbT2Gh8fpLHy4Rs3WXO9Vvud+SIqeEeGVZroOz3FSUyj1b3gTBeTVIXS4S5jIjZDFAkB3Z4WseDwyh8Wf1fAvCzaB/f7b4tRmkHCZeejSV3WABVh9MRTQIZeHfzGfOKVnBxc8ehiHuTjcRRzVfFn/xXVhAkEAi6ztiIfv9t6pONiesTq7l3caihGtPHz9SnaWLOTicQQtXlY6phK0Nd95KAUBw7wXejtB3sYfnd5nfm54FiJ9GQ==

암복호화 테스트

@Test
public void test() {
    KeyPair keyPair = Rsa.generateKey(2048);
    Optional.ofNullable(keyPair).ifPresent(k -> {
        PublicKey publicKey = k.getPublic();
        PrivateKey privateKey = k.getPrivate();

        Rsa rsaForPublic = new Rsa(publicKey);
        Rsa rsaForPrivate = new Rsa(privateKey);

        /*
        평문은 public key로 암호화하고, private key로 복호화 한다.

        RSA 는 반대로 public 복호화, private 암호화가 가능하지만,
        대부분 암호화 하는 키를 공개키(public)로, 복호화 하는 키를 개인키(private) 정의 하기 때문에
        public으로 암호화, private으로 복호화 한다.
            */

        String plain = "test 1234567890 가나다라마바사아자차카타파하";

        try {
            String encrypted = rsaForPublic.encrypt(plain);
            log.info("encrypted : {}", encrypted);
            String decrypted = rsaForPrivate.decrypt(encrypted);
            log.info("decrypted : {}", decrypted);
        } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
            e.printStackTrace();
        }
    });
}

결과

INFO sunghs.java.utils.cipher.RsaTest - encrypted : B2e7Y+II7eZtgOC7H1QULUnJbm2sme7KInVRBFSEeeA0nymC5zqMGNSr+eH3trRqkSCF03WS3KyUvOPyUVc8nLkGhxixUJcJvBB89guiw8oDoAnFxShSXGobWB12VOV2rNPj65gerhNe4hoKX3ACEbswVoxHnVfStSVVYsDsu2RInePmunHq1VPOBJog/Z+QOne8NE4vXL7dac8RrwAaw1aDiC2LaVhv0U4hbu/t3ufF7ezi+ZendGZUl/LlONeJpVxTIOerZM6ztWvTBuWQgrkpOywWo5DRcq/aFFzD4hXJEMFCw0NiUzwUasukFPhe10ScAvnLn0B4FoyX8/CFIw==

INFO sunghs.java.utils.cipher.RsaTest - decrypted : test 1234567890 가나다라마바사아자차카타파하

'Copy&Paste' 카테고리의 다른 글

[Java] HeapDump 파일 만들기  (0) 2021.03.17
[JAVA] Java Object List 중복제거  (6) 2021.01.04
[Java] SEED-128 양방향 암호화  (0) 2020.07.09
[Java] AES-128 양방향 암호화  (1) 2020.07.05
[Java] Hash 암호화 (MD5, SHA256)  (0) 2020.06.28

댓글