[Spring] AES 암호화 알고리즘에 대해

최동근·2023년 4월 6일
3

안녕하세요 오늘은 현재 가장 많이 사용되는 암호화 방식은 AES 을 알아보겠습니다 🐳
이번 포스팅은 제가 AES 에 공부하면서 같이 공부했던 내용들을 함께 정리하는 목적을 가집니다 ❗️

🔑 대칭키와 비대칭키

대칭키 암호화 방식

대칭키 란 암복호화에 사용하는 키가 동일한 암호화 방식입니다.
따라서 암복호화에 참여하는 쪽에서만 해당 키를 통해 데이터에 접근할 수 있습니다. 대표적인 대칭키 알고리즘으로는 DES,AES SEED 등이 있습니다.

대칭키 암호화 방식은 비대칭키 암호화 방식에 비해 속도가 빠르다는 장점이 있지만 암복호화에 사용되는 키가 하나이기 때문에 키를 교환해야하는 문제 가 발생합니다. 이는 키 탈취 문제를 야기할 수 있으며 해당 데이터를 복호화 해서 사용하는 사람이 증가할수록 따로따로 키 교환을 해야하기 때문에 관리해야 할 키가 방대하게 많아집니다 🥲

이러한 문제를 해결하기 위해 키를 사전에 공유하는 방법 , Diffie-Hellman 키 교환에 의한 방법 등 대칭키 암호화 방식을 해결하기 위한 여러가지 해결책이 제시되어 있습니다.

비대칭키 암호화 방식

비대칭키 란 암호화하는 키와 복호화 하는 키가 다른 암호화 방식입니다.
이해를 위해 간단한 예시를 들어보겠습니다.

만약 A 가 B 에게 보낼 데이터가 있어 데이터를 보낸다고 가정해봅시다.
이때, A 는 데이터를 암호화 해서 전송할 예정이고, 암호화 방식으로 비대칭키를 사용한다고 가정해봅시다 ❗️
여기서 필요한 키는 데이터를 암호화 하기 위한 키와 받은 데이터를 복호화 하기 위한 키가 필요합니다.
비대칭키 암호화 방식에서 암호화 하기 위한 키를 공개키 라고 하며, 복호화 하기 위한 키를 개인키 라고 합니다.

이름에서 알 수 있듯이 공개키 는 누구에게나 공개되어 데이터를 전송하기 전 암호화가 가능하지만 개인키 는 특정 데이터를 받는 쪽에서만 가지고 있으며 개인키 를 통해 전송 받은 데이터를 복호화 할 수 있습니다.

해당 이미지는 비대칭키 암호화 방식에서 사용된 예시를 도식화한 이미지입니다 👍

비대칭키 암호화 방식에 장점은 먼저 키를 교환할 필요가 없기 때문에 제 3 자가 키를 탈취할 일도 없으며, 만약 중간에 제 3자가 악의적인 의도를 위해 공개키를 얻어도 개인키로만 복호화가 가능하기에 기밀성을 제공합니다.

하지만, 대칭키 암호화 방식의 가장 큰 장점이였던 속도 측면에서 보았을 때 비대칭키 암호화 방식은 효율적이지 못합니다 🙃

🔑 AES 암호화 알고리즘에 대해

AES(Advanced Encryption Standard) 는 고급 암호화 표준이며 , 암호화 및 복호화 시 동일한 키를 사용하는 대칭키 알고리즘입니다.

AES 종류에는 AES-128,AES-192,AES-256 이 있고 각각 뒤에 붙은 숫자는 암호화 및 복호화에 사용되는 키의 길이입니다❗️
AES 는 높은 안정성과 빠른 속도로 현재 가장 대중적으로 사용되어 지고 있는 암호화 알고리즘입니다.

이번 포스팅에서는 AES-256 을 기준으로 진행하려고 합니다 🔥

Secret key

Secret key 는 암호화 할 대상인 평문을 암호화하는데 사용되며 절때로 외부에 노출되면 안됩니다. 만약 노출 된다면 암호화에 의미가 없습니다.
AES-256 은 256 bits 의 Secret key 를 사용합니다.

Block Cipher(=블록 암호)

암호학에서 블록 암호(Block Cipher) 는 기밀성 있는 정보를 정해진 블록 단위로 암호화하는 시스템입니다.

AES 방식의 암호화는 128 비트의 고정된 블록 단위로 암호화를 수행합니다 🧱
즉, 주어진 암호화 대상을 128 비트씩 나누어 블록 단위로 암호화를 진행하게 되는데 이렇게 블록 단위로 쪼개진것을 Block Cipher 라고 합니다.

또한, 이렇게 Block Cipher 를 사용할때 운용하는 방식을 Block Cipher Mode 라고 하는데 여기에는 CBC(Cipher Block Chaining),ECB(Elctronic Codebook Mode) 등이 있습니다.
보통 CBC 방식이 권장됩니다 ❗️

CBC 방식은 간단하게 이야기 해서 암호화를 이전 블록에 의존하도록 만드는 방식이며 이는 안전한 방식입니다. 여기에는 IV 라는 개념이 사용되는데 뒤에서 다시 살펴보겠습니다.

해당 이미지는 CBC 를 도식화한 이미지입니다.
집중해서 봐야할 부분은 암호화 대상인 각 plaintext 가 암호화가되어 ciphertext 가 되는데 이때 다음 plaintext 가 암호화 될때 이전에 암호화했던 블록과 XOR 연산을 한 다음에 암호화를 진행하게 됩니다 ⛓️
이때문에 같은 내용의 plaintext 라도 전혀다른 암호문을 갖게됩니다.

IV(initialization vector)

앞서 살펴보았던것처럼 AES 암호화 방식에서 Block Cipher Mode 에서 가장 많이 사용되는 것은 CBC 입니다.

그런데, 만약 첫번째 블록이라면 어떨까요? CBC 는 이전 암호화된 블록에 의존하는데 말이죠. 당연히 첫번째 블록은 이전 암호화된 블록이 존재하지 않습니다 🤔
이를 위해 첫번째 블록에는 IV 라는 것이 사용됩니다.
간단하게 이야기하면 생성된 128 비트 IV 값을 가지고 첫번째 블록을 암호화합니다.
매번 다른 IV 를 생성하면 같은 plaintext 라도 매번 다른 ciphertext 를 생성할 수 있겠죠?

Padding

Padding 을 이해하기 위해서는 AES 암호화 방식에 대해 알고있어야합니다.
앞서 이야기 했던 것처럼 AES 는 128 비트의 블록 단위로 암호화를 진행합니다.
그렇기에, input 데이터 길이는 보통 한 Block Size 인 128 bit의 배수이면 이상적일 것입니다 👍

하지만 어느 경우에서든지 암호화의 대상이 되는 데이터가 128 비트의 배수의 길이를 가진다는 것은 보장할 수 없습니다 ⛔️
따라서, AES 에서는 input 데이터의 길이가 128 비트의 배수가 아닌 경우 128 비트를 맞추기 위해 마지막 블록에 값을 추가합니다.

이때 추가되는 행위 또는 값을 Padding 이라고 합니다 ❗️
Padding 의 종류에는 PKCS5,PKCS7 등 여러가지가 있습니다.
각 종류에 대한 설명은 해당 포스팅을 참고 해주세요 👨‍💻

🔑 코프링(Kotlin + Spring) 을 이용한 AES-256 구현해보기

제가 진행했던 대출 심사 프로젝트 에서 각 유저에 주민번호 userRegistrationNumber는 민감한 정보라고 판단하여 DB 저장시 암호화 하였습니다.
이때 AES-256 을 적용하였습니다 ❗️

step 1

첫번째 단계로는 AES-256 암호화 기능을 하는 EncryptComponent 클래스 뼈대를 만들고 Bean 으로 등록해보겠습니다.

@Component 
class EncryptComponent (
	private val apiConfigurationProperties: ApiConfigurationProperties 
) {

	fun encryptString(encryptString: String): String {
		// encryption 관련 코드 구현 예정  
	}

	fun decryptString(decryptString: String): String {
		// decryption 관련 코드 구현 예정
	}

	fun cipherPkcs5(opMode: Int, secretKey: String) : Cipher {
		// AES를 사용하기 위한 다양한 설정 구현 예정
	}	
}

apiConfigurationPropertiesapplication.yml 파일에 있는 secret key 를 주입받기 위한 클래스입니다 ❗️

@ConstructorBinding
@ConfigurationProperties(prefix = "api.encrypt")
data class ApiConfigurationProperties(
  val secretKey: String
)
api:
encrypt:
  # VM 옵션을 통해 주입
  secretKey: ${api.encrypt.secretKey}

step 1 에서 작성한 클래스의 뼈대에는 크게 3가지 메소드가 존재합니다. 각각은 암호화/복호화/설정 관련 기능을 합니다 🔑

Step 2

두번째 단계에서는 cipherPkcs5 메소드를 작성해보겠습니다.

자바에서는 기본적으로 암호화 알고리즘을 위해 Cipher 클래스를 제공합니다. 해당 클래스는 javax.crypto.Cipher 에 위치합니다
또한, 메소드 이름에서도 유추할 수 있듯이 PKCS5 Padding 을 사용할 것입니다.

fun cipherPkcs5(opMode: Int, secretKey: String): Cipher {

	  // getInstance 메소드를 통해 Cipher 객체 반환 이때 
	  // CBC operation mode, PKCS5 padding 설정
      val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
      // SecretKeySpec 메소드를 통해 키 생성
      val sk = SecretKeySpec(this.apiConfigurationProperties.secretKey.toByteArray(Charsets.UTF_8), "AES")
      //
      val iv = IvParameterSpec(this.apiConfigurationProperties.secretKey.substring(0, 16).toByteArray(Charsets.UTF_8))

      c.init(opMode, sk, iv)
      return c
}

getInstance 메소드는 Cipher 객체를 반환합니다.
getInstance 메소드는 Cipher.getInstance(알고리즘 종류/모드/패딩) 형태를 가집니다 ❗️

SecretKeySpec 메소드는 비밀 키를 생성합니다.
해당 메소드는 (byte[] key, 알고리즘 종류) 형태를 가집니다.

IvParameterSpec 메소드는 IV Vector 를 생성하는 메소드입니다. CBC 에서 사용되며 처음 암호화될 블록을 위해 사용됩니다. 이때 각 블록의 크기는 128 비트(= 16 바이트) 이기 때문에 secretKey 의 substring 메소드를 이용하여 16개로 끊습니다.

이렇게 3가지 메소드를 호출 후 마지막으로 init 메소드를 통해 초기화를 진행합니다.

Step 3

세번째 단계에서는 encryptStringdecryptString 을 작성합니다.

fun encryptString(encryptString: String): String {

      val encryptedString = this.cipherPkcs5(Cipher.ENCRYPT_MODE, apiConfigurationProperties.secretKey)
          .doFinal(encryptString.toByteArray(Charsets.UTF_8)) // encryption

      return String(encoder.encode(encryptedString)) // base 54 encoding
  }
fun decryptString(decryptString: String): String {

      val byteString = decoder.decode(decryptString.toByteArray(Charsets.UTF_8)) // base 64 decoding
      return String(
          this.cipherPkcs5(Cipher.DECRYPT_MODE, apiConfigurationProperties.secretKey).doFinal(byteString)
      ) // decryption
  }

이때 암호화 encoding 하고 복호화 하기 decoding 하기 위해 Base 64 Encoder/Decoder 를 생성합니다.

  private val encoder = Base64.getEncoder()
  private val decoder = Base64.getDecoder()

이렇게 3단계를 거쳐 작성한 코드들을 하나로 합쳐보겠습니다 👏

@Component
class EncryptComponent(
    private val apiConfigurationProperties: ApiConfigurationProperties,
) {

    private val encoder = Base64.getEncoder()
    private val decoder = Base64.getDecoder()

    fun encryptString(encryptString: String): String {

        val encryptedString = this.cipherPkcs5(Cipher.ENCRYPT_MODE, apiConfigurationProperties.secretKey)
            .doFinal(encryptString.toByteArray(Charsets.UTF_8)) // encryption

        return String(encoder.encode(encryptedString)) // base 54 encoding
    }

    fun decryptString(decryptString: String): String {

        val byteString = decoder.decode(decryptString.toByteArray(Charsets.UTF_8)) // base 64 decoding
        return String(
            this.cipherPkcs5(Cipher.DECRYPT_MODE, apiConfigurationProperties.secretKey).doFinal(byteString)
        ) // decryption
    }

    fun cipherPkcs5(opMode: Int, secretKey: String): Cipher {
        val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
        val sk = SecretKeySpec(this.apiConfigurationProperties.secretKey.toByteArray(Charsets.UTF_8), "AES")
        val iv = IvParameterSpec(this.apiConfigurationProperties.secretKey.substring(0, 16).toByteArray(Charsets.UTF_8))

        c.init(opMode, sk, iv)
        return c
    }
}

이렇게 AES 암호화 방식에 대해 알아보았습니다.
개발자로써 보안은 굉장히 중요한 요소라고 생각합니다. 또한 이를 위해 개발자는 신경써야할 부분이 많으며 암호화 알고리즘에 정확한 이해가 필요하다고 생각합니다 👨‍💻

앞으로 다양한 코드에서 사용해봐야겠습니다 🔥


profile
비즈니스가치를추구하는개발자

0개의 댓글