본문 바로가기
자바

[Java] Cipher, 자바의 암호화&복호화를 담당하는 클래스

by 책 읽는 개발자_테드 2021. 4. 20.
반응형

아래 홈페이지에 정리된 내용을 의역하여 자바의 Cipher 클래스에 대하여 설명하는 글입니다.

www.baeldung.com/java-cipher-class

 

학습 목표 

- Cipher란?

- Cipher 객체 인스턴스화하기

- Keys

- Cipher 초기화(Initialization)

- Encrpytion/Decryption

- Providers

- 암호화, 복호화 테스트

- Thread-Safety


Cipher란?

 

암호화는 권한이 있는 사용자만 메세지를 이해하거나 접근할 수 있도록, 메세지를 인코딩하는 과정을 말한다.

 

여기서 plaintext라고 불리는 메세지는 암호화 알고리즘을 통해 암호화되어 cyphertext를 생성한다. cyphertext는 복호화를 통해 오직 권한이 있는 사용자만 읽을 수 있다. 

 

이 글은 자바에서 암호화, 복호화 기능을 제공하는 Cipher 클래스에 대해 설명한다.

 

javax.crypto pakage에 위치한 Cipher 클래스는 암호화 및 복호화 기능을 제공하며, JCE framework의 핵심을 구성한다.   

- Java Cryptography Extension(JCE): 자바 보안 기능의 핵심을 담당하는 Java Crpytography Architectur(JCA)의 일부분으로, 애플리케이션에서 데이터 암호화, 복호화 그리고 개인 데이터의 해싱을 제공한다.

 

 

Cipher 객체 인스턴스화하기

 

Cipher 객체를 인스턴스화 하기 위해서는 static getInstance 메서드를 호출하고, 원하는 변환 형태의 이름을 전달한다. 선택적으로 povider의 이름을 표시할 수도 있다. 이제 Cipher 클래스를 인스턴스화하는 예제를 작성해보자.

 

public class Encryptor {

   public byte[] encryptMessage(byte[] message, byte[] keyBytes) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {

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

 

AES/ECB/PKCS5Padding 변환은 getInstance 메서드에 Cipher 객체를 AES 암호화, CBC operation mode, PKCS5 padding scheme로 초기화하라고 요청한다. 

- 자바에서는 secretKey의 길이가 32bit면 AES-256, 24bit면 AES-192, 16bit의 경우 AES-128로 암호환한다.

- operation mode와 padding ↓

 

더보기

- ECB(Electronic Code Book) operation mode: 암호 알고리즘은 혼돈과 확산을 달성하기 위해 Substitution과 Permution을 이용한다. 이 둘을 연속으로 수행하는 것을 SPN(Substitution Permution Network)라고 한다. SPN을 이용하는 알고리즘은 보통 데이터를 블록 단위로 나눠서 처리한다. operation mode는 나누어져 처리된 블록을 합치는 것을 의미한다. 

  - 혼돈(confusion): 암호문으로부터 키를 알아낼 수 없게 하는 성질이다. 

  - 확산(diffusion): 암호문으로부터 원문을 알아낼 수 없게 하는 성질이다.

  - Substitution: 문자를 다른 문자로 바꾸는 것이다.

  - Permutation: 문자들의 순서를 바꾸는 것이다.

 

operation mode 중 ECS는 단순히 블록 단위로 처리한 결과를 이어 붙이는 방법이다. 때문에 원문의 패턴이 그대로 드러난다. 이는 암호문에서 원물을 유추할 수 있어 '확산'의 성질을 달성하지 못한다고 볼 수 있다.

 

CBC(cipher-block chaining)는 원문 블록을 그대로 암호화하지 않고, 직전에 암호화된 블록과 XOR 연산을 한 다음 암호화를 수행한다. 그리고 첫 번째 블록은 직전 암호문이 없으므로 XOR 연산 대상을 위한 초기화 벡터를 입력받는다. 이때, 초기화 벡터는 블록 사이즈와 동일하며, 매번 다른 값이 제공되어서 같은 원문 블록이라도 전혀 다른 암호문을 갖게 된다.

 

CBC 모드는 직전 블록이 다음 블록의 암호화에 관여하므로 일부 블록만 복호화하고 싶어도 전체를 복호화해야 한다. 반면 ECB는 전체를 복호화하지 않고 일부만 복호화하는 것이 가능하다.

 

- PKCS5 padding: 원문의 크기가 블록 하나의 크기의 배수가 아니라면, 마지막 블록의 빈 부분이 생긴다. 패딩 기법이란 데이터를 특정 크기로 맞추기 위해서, 특정 크기보다 부족한 부분의 공간을 의미 없는 문자들로 채워서 비트수를 맞추는 것이다.

 

PKCS5는 단순히 패딩 크기의 값을 갖는 바이트를 크기만큼 반복하는 방식이다. 이때 PCKCS5의 경우 암호 블록 사이즈가 8바이트의 고정 길이가 된다. (해당 패딩은 AES와 함께 사용할 수 없다.)

 

PKCS7은 AES 같은 현대적인 암호화 알고리즘이 128, 192, 256 같은 긴  길이의 키를 사용하며, 블록 크기가 8이 아닌 16bytes를 사용하므로 나오게 된 표준이다. 암호 블록 사이즈가 최대 16바이트까지 가능하며, 최대 가능한 패딩 크기 255바이트로 1~255바이트의 가변 길이를 갖는다.

 

자바에서는 PKCS7Padding이 없고, 내부적으로 PKCS5가 PKCS&로 수행된다.

 

*operation mode와 padding의 더 자세한 내용은 다음 블로그를 참고하자.

http://happinessoncode.com/2019/04/06/java-cipher-algorithm-mode-padding/

 

이런 작업을 생략하고, Cipher 객체를 오직 변환 알고리즘만 명시하여 초기화할 수도 있다.

 

Cipher cipher = Cipher.getInstance("AES");

 

이러한 경우 자바는 mode와 padding scheme를 위해 provider-specific default values를 사용한다. 

 

만약 요청한 변환이 provider가 지원하지 않거나 null, empty, 부정확한 포맷이면, NoSuchAlgorithmExceptio을 던진다. 또한 변환이 지원하지 않는 padding scheme를 포함한다면, NoSuchPaddingException을 던진다. 

 

Keys

 

Key 인터페이스는 암호화 작업들을 위한 키들을 나타낸다. Keys는 인코딩된 키, 키의 인코딩 포맷, 암호화 알고리즘을 포함하는 불투명한 컨테이너다.

 

키는 일반적으로 키 팩토리를 사용하는 key generators, certificates, 또는 key specifications를 통해서 획득할 수 있다. key bytes로 부터 양방향 키를 생성해보자.

 

SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");



Cipher 초기화(Initialization)

 

Cipher 객체를 초기화하기 위해서 키 또는 증명서(Certificate) 그리고 Cipher의 작동 모드를 나타내는 opmode와 함께 init() 메서드를 호출할 수 있다.

 

선택적으로, 랜덤과 관련된 소스를 전달할 수 있다. 기본적으로 가장 높은 우선 순위로 설치된 provider의 SecureRandom 구현이 사용된다.

 

알고리즘 별 매개 변수 세트를 선택적으로 지정할 수도 있다. 예를 들어, IvParameterSpec을 전달하여 초기화 벡터를 지정할 수 있다.

 

다음은 이용 가능한 cipher 작업 모드 목록이다.

  • ENCRYPT_MODE: cipher 객체를 암호화 모드로 초기화한다. 
  • DECRYPT_MODE: cipher 객체를 복호화 모드로 초기화한다. 
  • WRAP_MODE: cipher 객체를 key-wrapping 모드로 초기화한다. 
  • UNWRAP_MODE: cipher 객체를  key-unwrapping 모드로 초기화한다. 

 

Cipher 객체를 다음처럼 초기화할 수 있다.

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);

 

만약 공급된 키가 길이나 인코딩이 적합하지 못해서 cipher를 초기화하는데 부적절하다면, init() 메서드는  InvalidKeyException을 던진다.

 

또한 cipher가 키에서 확인할 수 있는 특정 알고리즘 매개변수가 필요하거나 키가 허용 가능한 최대 크기(설정된 JCE 관할 정책 파일에 따라 결정됨)를  초과하는 경우에도 예외를 던진다.

 

다음은 인증서(Certificate)를 사용하는 예제다.

public byte[] encryptMessage(byte[] message, Certificate certificate) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {

   Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
   cipher.init(Cipher.ENCRYPT_MODE, certificate);

   //...
}

 

Cipher 객체는 데이터 암호화를 위해 getPublicKey() 메서드를 호출하여 인증서(certificate)로 부터 공용키를 얻는다. 

 

Encrpytion/Decryption

 

Cipher 객체를 초기화한 후에, 암호화 또는 복호화 작업을 위해 doFinal() 메서드를 호출할 수 있다. 해당 메서드는 암호화 또는 복호화된 메세지를 포함한 byte 배열을 반환한다.

 

앞서 생성한 encryptMessage() 메서드에서 doFinal() 메서드를 호출해보자. 

public byte[] encryptMessage(byte[] message, byte[] keyBytes) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException {

   Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
   SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
   cipher.init(Cipher.ENCRYPT_MODE, secretKey);

   return cipher.doFinal(message);
}

 

복호화 작업을 수행하기 위해서, opmode를 DECRYPT_MODE로 변경해보자.

public byte[] decryptMessage(byte[] encryptedMessage, byte[] keyBytes) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException{

   Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
   SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
   cipher.init(Cipher.DECRYPT_MODE, secretKey);
   
   return cipher.doFinal(encryptedMessage);
}

 

Providers

 

provider 기반 아키텍처를 사용하도록 설계된 JCE는 BouncyCastle과 같은 인증된 암호화 라이브러리를 보안 공급자(provider)로 연결하고, 새로운 알고리즘을 원활하게 추가 할 수 있도록한다.

 

모든 프로바이더는 java.security.Provider 클래스의 구현체다. 이 프로바이더 구현체는 보안 알고리즘 구현체 목록을 포함하고 있다. 특정 알고리즘의 인스턴스가 필요해지면, JCA 프레임워크는 프로바이더 저장소에서 해당 알고리즘의 적합한 구현체 클래스를 찾아 클래스 인스턴스를 생성한다.

 

이제 security provider로 BouncyCastle을 설정해보자. 아래 홈페이지에서 본인의 자바 버전의 알맞은 jar 파일을 다운로드 하자.

 

http://www.bouncycastle.org/latest_releases.html

 

 

jar 파일을 다운로드 했으면, 자바 프로젝트에 추가하자. 인텔리제이를 기준으로 File - Project Structue - Libraries에 다운로드한 jar 파일을 다운로드하면 된다.

 

자바 프로젝트에 다운로드한 jar 파일을 추가했다면, 아래와 같은 코드로 security provider로 BouncyCastle을 추가할 수 있다.

Security.addProvider(new BouncyCastleProvider());

 

이 처럼 BouncyCastle를 security provider로 추가하면, cipher를 초기화하는 동안에 provider를 지정할 수 있다.

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "BC");

 

BC는 BouncyCastle를 provider로 지정한다. Security.getProviders() 메서드로 등록된 provider의 목록을 얻을 수 있다.

 

public class Encryptor {

   public static void main(String[] args) {

       Provider[] providers = Security.getProviders();

       for (Provider provider : providers) {
           System.out.println(provider.getName());
       }
   }
}

 

결과



암호화, 복호화 테스트

 

메시지를 암호화, 복호화하는 테스트를 작성해보자. 해당 테스트에서는 128-bit의 AES 암호화 알고리즘을 사용하고, 복호화한 결과가 원본 메시지 텍스트와 같다는 단언문을 작성한다.



import org.junit.Test;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class EncryptorTest {


   @Test
   public void 암호화하고복호화하면_복호하된것은원본과같다() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

       String encryptionKeyString = "thisisa128bitkey";
       String originalMessage = "암호화 메시지 입니다.";
       byte[] encryptionKeyBytes = encryptionKeyString.getBytes(StandardCharsets.UTF_8);
       
       Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
       SecretKey secretKey = new SecretKeySpec(encryptionKeyBytes, "AES");
       cipher.init(Cipher.ENCRYPT_MODE, secretKey);
       byte[] encrpyedMessageBytes = cipher.doFinal(originalMessage.getBytes());
       cipher.init(Cipher.DECRYPT_MODE, secretKey);
       byte[] decryptedMessageBytes = cipher.doFinal(encrpyedMessageBytes);

       assert(originalMessage).equals(new String(decryptedMessageBytes));
   }
}

 

결과



Thread-Safety

Cipher는 어떠한 내부 동기화 형태가 없는 상태가 있는 클래스다. 사실 Cipher의 init() 또는 update() 메소드는 Cipher 객체의 내부 상태를 부분적으로 변경한다.

 

때문에 Cipher 클래스는 thread-safe가 아니고, 암호화/복호화의 필요 당 하나의 Cipher 객체를 생성해야 만 한다.

 

참조

d2.naver.com/helloworld/197937

반응형

댓글