필기노트

JAVA 암호화와 복호화 Cipher 본문

JAVA

JAVA 암호화와 복호화 Cipher

우퐁코기 2023. 3. 1. 09:03
반응형

먼저 암호화라는 개념은 너무나 간단합니다

내가 가진 원문의 메세지를 상대방이 해석할 수 없게 하는 것이 바로 암호화의 목적

 

javax.crypto.Cipher 클래스는 암호화 알고리즘을 나타낸다.

암호를 사용하여 데이터를 암호화하거나 복호화할 수 있다.

아래와 같이 암호화 알고리즘, 운용 방식 그리고 패딩 방식을 전달해 Cipher 인스턴스를 만들 수 있다.

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

위의 예제 코드에서 전달한 파라미터에 대해서 간단히 알아보자.

각각 순서대로 암호화 알고리즘, 운용 방식 그리고 패딩 방식을 나타낸다.

 

(1) 암호화 알고리즘

암호화에 사용되는 알고리즘을 말한다. 암호화 알고리즘은 크게 단방향 알고리즘과 양방향 알고리즘으로 나눌 수 있으며 양방향 알고리즘은 대칭키 방식과 비대칭키 방식으로 구분할 수 있다.

단방향 알고리즘의 경우 평문을 암호문으로 암호화할 수 있지만, 반대로 암호문을 평문으로 되돌리는 복호화는 불가능하다. 보통 해시(Hash) 기법을 사용하며 SHA-256, MD-5등이 있다.

비대칭키 알고리즘암호화와 복호화에 사용되는 키가 서로 다르다. 두 개의 키 중에서 하나는 반드시 공개되어야 사용이 가능하기 때문에 공개키 방식이라고도 한다. 대표적으로는 RSA가 있다.

대칭키 알고리즘암호화할 때 사용되는 키와 복호화할 때 사용되는 키가 동일한 암호화 방법을 말한다. 가장 보편적으로 사용되는 알고리즘으로 AES가 있다.

 

(2) 운용 방식

암호학에서 특정 비트 수의 집합을 한꺼번에, 그러니까 일정 크기의 블록 단위로 구성하여 처리하는 암호 기법을 블록 암호(block cipher)라고 한다.

그냥 요약하면, 특정 블록 단위로 암호화가 이루어진다 라고 생각하면 됩니다

 

Block Cipher Mode에는 크게 2가지가 있는데요

ECB

각각의 Block 들이 같은 Key 를 가지고 암호화를 진행하게 되는 것입니다

그렇기 때문에 보안에 취약하죠

왜냐하면 ECB 를 적용하게 되면, 아까 같은

"Hello World" -> "bc6fdsfjiowejklsdfm92" 

연산을 매번 할때마다 동일하게 나오게 되는 것입니다!

 

CBC

아래는 위의 코드에서 사용한 CBC(Cipher Block Chaining) 운용 방식이다. CBC 모드를 사용한 암호화 과정에서는 원문의 각 블록은 암호화되기 전에 이전 암호문 블록과 XOR 연산되는 방식이다. 따라서 같은 내용의 원문 블록이어도 다른 암호문을 갖는다.

 

여기서 초기화 벡터(Initialization Vector)라는 용어가 등장한다. 최초의 평문 블록을 암호화할 때 직전의 암호문 블록이 없기 때문에 이를 대체할 블록이 필요한데, 이를 초기화 벡터라고 하며 영문자 앞 글자만 따서 IV로도 표기한다.

 

(3) 패딩

AES나 DES와 같은 블록 암호 알고리즘은 평문의 길이가 해당 암호의 블록 크기(DES는 8바이트, AES는 16바이트)의 배수로 정확하게 떨어져야 한다. 그렇지 않은 경우 가장 마지막 블록은 정해진 블록 크기보다 작은 크기로 구성된다. 이때, 마지막 블록의 빈 공간을 채우는 것을 패딩이라고 한다. 물론 특정 바이트의 배수여도 패딩 방식은 추가해야 한다.

 

대표적으로 8바이트로 고정된 PKCS5와 가변 크기의 PKCS7 등이 있으며 각각 DES와 AES 알고리즘에 사용한다.

위의 Cipher 객체를 얻어오는 코드를 다시 봐보자. AES 알고리즘을 사용했는데 PKCS5 패딩 방식을 적용한 것을 보고 혼동이 될 수 있다. 자바 프로그래밍에서 패딩 방식을 입력할 때는 PKCS5와 PKCS7을 구분하지 않고 PKCS5Paddig로 입력한다. 내부적으로 가변 크기인 PKCS7 패딩 방식으로 동작하지만, 네이밍이 PKCS5로 되있다.

 

암호화 및 복호화 예제

데이터를 암호화하거나 복호화 하려면 키가 필요하다. 앞서 살펴본 것처럼 사용하는 알고리즘의 유형에 따라서 대칭키와 비대칭 키가 존재한다. 자바에서는 javax.crypto.KeyGenerator 클래스를 이용하면 암호화에 필요한 키를 생성할 수 있다.

public AESCryptoUtil {

    /**
     * 키 반환
     */
    public static SecretKey getKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128);
        SecretKey secretKey = keyGenerator.generateKey();
        return secretKey;
    }
	
    /**
     * 초기화 벡터 반환
     */
    public static IvParameterSpec getIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }
    
    // 이어지는 암호화 및 복호화 예제 코드 
}
  • KeyGenerator.getInstance("AES"); // 지정된 알고리즘에 대한 비밀 키를 생성하는 KeyGenerator 객체를 반환합니다.
  • keyGenerator.init(128); // 특정 키 크기에 대해 KeyGenerator를 초기화합니다.
  • SecretKey secretKey = keyGenerator.generateKey(); // 비밀 키를 생성합니다.
  • 암호화 알고리즘은 AES를 사용하므로 아래와 같이 초기화 벡터(Initialization Vector)에 대한 코드 정의도 필요하다.
  • IvParameterSpec // 이 클래스는 초기화 벡터 (IV)를 지정합니다.
  • new SecureRandom().nextBytes(iv); // 사용자가 지정한 바이트수의 난수 바이트를 생성합니다.

 

생성한 키는 Cipher 클래스 객체의 init 메서드의 인자로 전달되어 Cipher 객체를 초기화하는데 사용한다.

public class AESCryptoUtil {
	
	// ... getKey, getIv 메서드는 생략
	
	public static String encrypt(String specName, SecretKey key, IvParameterSpec iv,
		String plainText) throws Exception {
		Cipher cipher = Cipher.getInstance(specName);
		cipher.init(Cipher.ENCRYPT_MODE, key, iv);
		byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
		return new String(Base64.getEncoder().encode(encrypted));
	}

	public static String decrypt(String specName, SecretKey key, IvParameterSpec iv,
		String cipherText) throws Exception {
		Cipher cipher = Cipher.getInstance(specName);
		cipher.init(Cipher.DECRYPT_MODE, key, iv); // 모드가 다르다.
		byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
		return new String(decrypted, StandardCharsets.UTF_8);
	}
}
  • 자바에서 문자열을 암호화하고 복호화 해보자. 먼저, 키와 초기화 벡터를 생성하고
  • Cipher.getInstance 메서드로 지정된 변환을 구현하는 Cipher 클래스의 인스턴스를 생성해야 한다. 
  • 그리고 앞서 만든 키와 초기화 벡터로 Cipher 인스턴스를 초기화시키는 과정이 필요하다.
  • Cipher를 초기화할 때 사용되는 파라미터 값은 암호화, 복호화에 따라서 다르므로 유의하자. 암호화를 할 때는 Cipher.ENCRYPT_MODE이며, 복호화를 할 떄는 Cipher.DECRYPT_MODE를 전달해야 한다.
  • 마지막으로 doFinal 메서드를 호출해서 문자열을 암호화하면 된다.
  • Base64.getEncoder().encode(encrypted) // Base64 인코딩 스키마를 사용하여 지정된 바이트 배열의 모든 바이트를 새로 할당된 바이트 배열로 인코딩합니다
  • new String 대신에 해당 메소드로 변경 가능하다 -> encodeToString(byte[] src) // Base64 인코딩 스키마를 사용하여 지정된 바이트 배열을 문자열로 인코딩합니다
  • 복호화는 암호화의 역순으로 가면 된다.

 

아래는 테스트 코드이다.

String plainText = "Hello, MadPlay!";

SecretKey key = AESCryptoUtil.getKey();
IvParameterSpec ivParameterSpec = AESCryptoUtil.getIv();
String specName = "AES/CBC/PKCS5Padding";

String encryptedText = AESCryptoUtil.encrypt(specName, key, ivParameterSpec, plainText);
String decryptedText = AESCryptoUtil.decrypt(specName, key, ivParameterSpec, encryptedText);

System.out.println("cipherText: " + encryptedText);
System.out.println("plainText: " + decryptedText);

출력 결과는 아래와 같다.

cipherText: vzyKxKufZmKdtSUwVKWJYg==
plainText: Hello, MadPlay!

 

파일 암호화와 복호화

public static void encryptFile(String specName, SecretKey key, IvParameterSpec iv,
        File inputFile, File outputFile) throws Exception {
	
    Cipher cipher = Cipher.getInstance(specName);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    
    try (FileOutputStream output = new FileOutputStream(outputFile);
        CipherOutputStream cipherOutput = new CipherOutputStream(output, cipher)) {
    
        String data = Files.lines(inputFile.toPath()).collect(Collectors.joining("\n"));
	    cipherOutput.write(data.getBytes(StandardCharsets.UTF_8));
    }
}

public static void decryptFile(String specName, SecretKey key, IvParameterSpec iv,
        File encryptedFile, File decryptedFile) throws Exception {
	
    Cipher cipher = Cipher.getInstance(specName);
    cipher.init(Cipher.DECRYPT_MODE, key, iv);

    try (
        CipherInputStream cipherInput = new CipherInputStream(new FileInputStream(encryptedFile), cipher);
        InputStreamReader inputStream = new InputStreamReader(cipherInput);
        BufferedReader reader = new BufferedReader(inputStream);
        FileOutputStream fileOutput = new FileOutputStream(decryptedFile)) {

        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
	    fileOutput.write(sb.toString().getBytes(StandardCharsets.UTF_8));
    }
}
  • CipherOutputStream(output, cipher) // CipherOutputStream은 OutputStream과 Cipher로 구성되어 write() 메서드가 데이터를 쓰기 전에 먼저 데이터 암호화를 시도합니다.
  • FileInputStream // 암호화된 파일을 읽어온다.
  • CipherInputStream // 먼저 데이터 복호화를 시도합니다.(바이트->문자)
  • InputStreamReader // 바이트 스트림에서 문자 스트림으로의 다리입니다.
  • BufferedReader // 최고의 효율성을 위해 

 

테스트는 아래 코드로 해볼 수 있다. 실행 결과로 입력에 사용된 파일과 복호화된 파일의 내용을 표준 출력으로 보여준다.

SecretKey key = AESCryptoUtil.getKey();
String specName = "AES/CBC/PKCS5Padding";
IvParameterSpec ivParameterSpec = AESCryptoUtil.getIv();

File inputFile = Paths.get("input.txt").toFile();
File encryptedFile = new File("encrypted.txt");
File decryptedFile = new File("decrypted.txt");
AESCryptoUtil.encryptFile(specName, key, ivParameterSpec, inputFile, encryptedFile);
AESCryptoUtil.decryptFile(specName, key, ivParameterSpec, encryptedFile, decryptedFile);

// 결과 확인용
String inputText = Files.lines(Paths.get("input.txt"), StandardCharsets.UTF_8)
    .collect(Collectors.joining("\n"));
String encryptedText = Files.lines(Paths.get("decrypted.txt"), StandardCharsets.UTF_8)
    .collect(Collectors.joining("\n"));

System.out.println("input: " + inputText);
System.out.println("decrypted: " + encryptedText);

결과

 

 

 


REFERENCE

 

자바 암호화와 복호화

자바에서 암호화와 복호화는 어떻게 구현할까? 암호화에 사용되는 알고리즘, 운용 방식, 패딩이란 무엇일까?

madplay.github.io

 

Java - AES Cipher 를 이용한 대칭키 암호화 방식

암호화 최근에 암호화에 대해서 많이 배우고 있었는데요 모르고 헷갈리고 정신없는 부분들이 너무 많아서 조금 이해하기 쉽게(?) 정리하면 좋겠어가지고 정리하게 되었습니다 사실 네이버 블로

huisam.tistory.com

반응형
Comments